kempo-server 2.2.0 → 3.0.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/CONFIG.md +295 -187
- package/README.md +5 -4
- package/SPA.md +14 -14
- package/dist/defaultConfig.js +1 -1
- package/dist/index.js +1 -1
- package/dist/render.js +2 -0
- package/dist/router.js +1 -1
- package/dist/serveFile.js +1 -1
- package/dist/templating/index.js +1 -0
- package/dist/templating/parse.js +1 -0
- package/docs/caching.html +103 -17
- package/docs/cli-utils.html +102 -16
- package/docs/configuration.html +104 -17
- package/docs/examples.html +104 -17
- package/docs/fs-utils.html +102 -16
- package/docs/getting-started.html +104 -17
- package/docs/index.html +176 -81
- package/docs/middleware.html +104 -17
- package/docs/request-response.html +104 -17
- package/docs/routing.html +104 -17
- package/docs/templating.html +292 -0
- package/docs-src/.config.js +11 -0
- package/docs-src/caching.page.html +220 -0
- package/docs-src/cli-utils.page.html +71 -0
- package/docs-src/configuration.page.html +310 -0
- package/docs-src/default.template.html +35 -0
- package/docs-src/examples.page.html +192 -0
- package/docs-src/fs-utils.page.html +102 -0
- package/docs-src/getting-started.page.html +63 -0
- package/docs-src/index.page.html +79 -0
- package/docs-src/middleware.page.html +133 -0
- package/docs-src/nav.fragment.html +73 -0
- package/docs-src/request-response.page.html +96 -0
- package/docs-src/routing.page.html +73 -0
- package/docs-src/templating.page.html +188 -0
- package/llms.txt +97 -31
- package/package.json +5 -2
- package/scripts/build.js +22 -1
- package/scripts/render.js +58 -0
- package/src/defaultConfig.js +14 -2
- package/src/index.js +1 -1
- package/src/router.js +69 -10
- package/src/serveFile.js +27 -0
- package/src/templating/index.js +132 -0
- package/src/templating/parse.js +285 -0
- package/tests/cacheConfig.node-test.js +2 -2
- package/tests/config-flag.node-test.js +61 -25
- package/tests/customRoute-outside-root.node-test.js +1 -1
- package/tests/router-wildcard.node-test.js +47 -2
- package/tests/templating-parse.node-test.js +243 -0
- package/tests/templating-render.node-test.js +188 -0
- package/tests/utils/test-scenario.js +4 -4
- package/docs/.config.json.example +0 -29
- package/docs/api/_admin/cache/DELETE.js +0 -28
- package/docs/api/_admin/cache/GET.js +0 -53
- package/docs/api/user/[id]/GET.js +0 -15
- package/docs/api/user/[id]/[info]/DELETE.js +0 -12
- package/docs/api/user/[id]/[info]/GET.js +0 -17
- package/docs/api/user/[id]/[info]/POST.js +0 -18
- package/docs/api/user/[id]/[info]/PUT.js +0 -19
- package/docs/init.js +0 -2
- package/docs/nav.inc.html +0 -70
package/README.md
CHANGED
|
@@ -207,7 +207,7 @@ export default async function(request, response) {
|
|
|
207
207
|
|
|
208
208
|
## Configuration
|
|
209
209
|
|
|
210
|
-
Kempo Server can be customized with a
|
|
210
|
+
Kempo Server can be customized with a `.config.js` file (or `.config.json` as fallback) to control caching, middleware, security, routing, templating, and more.
|
|
211
211
|
|
|
212
212
|
For detailed configuration options and examples, see **[CONFIG.md](./CONFIG.md)**.
|
|
213
213
|
|
|
@@ -216,10 +216,10 @@ For detailed configuration options and examples, see **[CONFIG.md](./CONFIG.md)*
|
|
|
216
216
|
Quick start:
|
|
217
217
|
```bash
|
|
218
218
|
# Create a config file INSIDE the server root
|
|
219
|
-
echo '{
|
|
219
|
+
echo 'export default { cache: { enabled: true } };' > public/.config.js
|
|
220
220
|
|
|
221
221
|
# Use different configs for different environments
|
|
222
|
-
kempo-server --root public --config dev.config.
|
|
222
|
+
kempo-server --root public --config dev.config.js
|
|
223
223
|
```
|
|
224
224
|
|
|
225
225
|
## Features
|
|
@@ -235,10 +235,11 @@ kempo-server --root public --config dev.config.json
|
|
|
235
235
|
- **Static File Serving** - Automatically serves static files with proper MIME types
|
|
236
236
|
- **HTML Routes** - Support for both JavaScript and HTML route handlers
|
|
237
237
|
- **Query Parameters** - Easy access to URL query parameters
|
|
238
|
-
- **Configurable** - Customize behavior with a
|
|
238
|
+
- **Configurable** - Customize behavior with a `.config.js` file
|
|
239
239
|
- **Security** - Built-in protection against serving sensitive files plus security headers middleware
|
|
240
240
|
- **Performance** - Smart file system caching, rescan optimization, and optional compression
|
|
241
241
|
- **Programmatic Rescan** - Trigger a file rescan from anywhere in the Node process without restarting
|
|
242
|
+
- **Templating** - XML-based templating with templates, pages, fragments, variables, conditionals, and loops
|
|
242
243
|
|
|
243
244
|
## Examples
|
|
244
245
|
|
package/SPA.md
CHANGED
|
@@ -16,7 +16,7 @@ Requests to `/pages/*.html` are **not** redirected, so the fragments are still s
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
spa/
|
|
19
|
-
├─ .config.
|
|
19
|
+
├─ .config.js ← server config (routes + caching)
|
|
20
20
|
├─ app.html ← shell page (loaded for every route)
|
|
21
21
|
├─ spa.js ← client-side routing logic
|
|
22
22
|
└─ pages/
|
|
@@ -29,24 +29,24 @@ spa/
|
|
|
29
29
|
|
|
30
30
|
## Configuration
|
|
31
31
|
|
|
32
|
-
Create a `.config.
|
|
32
|
+
Create a `.config.js` in your SPA root:
|
|
33
33
|
|
|
34
|
-
```
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
```javascript
|
|
35
|
+
export default {
|
|
36
|
+
customRoutes: {
|
|
37
|
+
'/kempo.css': '../node_modules/kempo-css/dist/kempo.min.css',
|
|
38
|
+
'/*.html': './app.html',
|
|
39
|
+
'/': './app.html'
|
|
40
40
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
middleware: {
|
|
42
|
+
security: {
|
|
43
|
+
enabled: true,
|
|
44
|
+
headers: {
|
|
45
|
+
'Cache-Control': 'public, max-age=3600'
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
}
|
|
49
|
+
};
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
- `"/*.html": "./app.html"` — the `*` wildcard matches a single path segment, so it catches `/about.html` but not `/pages/about.html`.
|
package/dist/defaultConfig.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default{allowedMimes:{html:{mime:"text/html",encoding:"utf8"},htm:{mime:"text/html",encoding:"utf8"},shtml:{mime:"text/html",encoding:"utf8"},css:{mime:"text/css",encoding:"utf8"},xml:{mime:"text/xml",encoding:"utf8"},gif:{mime:"image/gif",encoding:"binary"},jpeg:{mime:"image/jpeg",encoding:"binary"},jpg:{mime:"image/jpeg",encoding:"binary"},js:{mime:"application/javascript",encoding:"utf8"},mjs:{mime:"application/javascript",encoding:"utf8"},json:{mime:"application/json",encoding:"utf8"},webp:{mime:"image/webp",encoding:"binary"},png:{mime:"image/png",encoding:"binary"},svg:{mime:"image/svg+xml",encoding:"utf8"},svgz:{mime:"image/svg+xml",encoding:"utf8"},ico:{mime:"image/x-icon",encoding:"binary"},webm:{mime:"video/webm",encoding:"binary"},mp4:{mime:"video/mp4",encoding:"binary"},m4v:{mime:"video/mp4",encoding:"binary"},ogv:{mime:"video/ogg",encoding:"binary"},mp3:{mime:"audio/mpeg",encoding:"binary"},ogg:{mime:"audio/ogg",encoding:"binary"},wav:{mime:"audio/wav",encoding:"binary"},woff:{mime:"font/woff",encoding:"binary"},woff2:{mime:"font/woff2",encoding:"binary"},ttf:{mime:"font/ttf",encoding:"binary"},otf:{mime:"font/otf",encoding:"binary"},eot:{mime:"application/vnd.ms-fontobject",encoding:"binary"},pdf:{mime:"application/pdf",encoding:"binary"},txt:{mime:"text/plain",encoding:"utf8"},webmanifest:{mime:"application/manifest+json",encoding:"utf8"},md:{mime:"text/markdown",encoding:"utf8"},csv:{mime:"text/csv",encoding:"utf8"},doc:{mime:"application/msword",encoding:"binary"},docx:{mime:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",encoding:"binary"},xls:{mime:"application/vnd.ms-excel",encoding:"binary"},xlsx:{mime:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",encoding:"binary"},ppt:{mime:"application/vnd.ms-powerpoint",encoding:"binary"},pptx:{mime:"application/vnd.openxmlformats-officedocument.presentationml.presentation",encoding:"binary"},avif:{mime:"image/avif",encoding:"binary"},wasm:{mime:"application/wasm",encoding:"binary"}},disallowedRegex:["^/\\..*","\\.config$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","config\\.php$","wp-config\\.php$","\\.DS_Store$","package\\.json$","package-lock\\.json$"],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:[]},maxBodySize:1048576,cache:{enabled:!1,maxSize:100,maxMemoryMB:50,ttlMs:3e5,maxHeapUsagePercent:70,memoryCheckInterval:3e4,watchFiles:!0,enableMemoryMonitoring:!0}};
|
|
1
|
+
export default{allowedMimes:{html:{mime:"text/html",encoding:"utf8"},htm:{mime:"text/html",encoding:"utf8"},shtml:{mime:"text/html",encoding:"utf8"},css:{mime:"text/css",encoding:"utf8"},xml:{mime:"text/xml",encoding:"utf8"},gif:{mime:"image/gif",encoding:"binary"},jpeg:{mime:"image/jpeg",encoding:"binary"},jpg:{mime:"image/jpeg",encoding:"binary"},js:{mime:"application/javascript",encoding:"utf8"},mjs:{mime:"application/javascript",encoding:"utf8"},json:{mime:"application/json",encoding:"utf8"},webp:{mime:"image/webp",encoding:"binary"},png:{mime:"image/png",encoding:"binary"},svg:{mime:"image/svg+xml",encoding:"utf8"},svgz:{mime:"image/svg+xml",encoding:"utf8"},ico:{mime:"image/x-icon",encoding:"binary"},webm:{mime:"video/webm",encoding:"binary"},mp4:{mime:"video/mp4",encoding:"binary"},m4v:{mime:"video/mp4",encoding:"binary"},ogv:{mime:"video/ogg",encoding:"binary"},mp3:{mime:"audio/mpeg",encoding:"binary"},ogg:{mime:"audio/ogg",encoding:"binary"},wav:{mime:"audio/wav",encoding:"binary"},woff:{mime:"font/woff",encoding:"binary"},woff2:{mime:"font/woff2",encoding:"binary"},ttf:{mime:"font/ttf",encoding:"binary"},otf:{mime:"font/otf",encoding:"binary"},eot:{mime:"application/vnd.ms-fontobject",encoding:"binary"},pdf:{mime:"application/pdf",encoding:"binary"},txt:{mime:"text/plain",encoding:"utf8"},webmanifest:{mime:"application/manifest+json",encoding:"utf8"},md:{mime:"text/markdown",encoding:"utf8"},csv:{mime:"text/csv",encoding:"utf8"},doc:{mime:"application/msword",encoding:"binary"},docx:{mime:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",encoding:"binary"},xls:{mime:"application/vnd.ms-excel",encoding:"binary"},xlsx:{mime:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",encoding:"binary"},ppt:{mime:"application/vnd.ms-powerpoint",encoding:"binary"},pptx:{mime:"application/vnd.openxmlformats-officedocument.presentationml.presentation",encoding:"binary"},avif:{mime:"image/avif",encoding:"binary"},wasm:{mime:"application/wasm",encoding:"binary"}},disallowedRegex:["^/\\..*","\\.config\\.js$","\\.config\\.json$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","config\\.php$","wp-config\\.php$","\\.DS_Store$","package\\.json$","package-lock\\.json$","\\.template\\.html$","\\.fragment\\.html$","\\.page\\.html$"],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:[]},maxBodySize:1048576,cache:{enabled:!1,maxSize:100,maxMemoryMB:50,ttlMs:3e5,maxHeapUsagePercent:70,memoryCheckInterval:3e4,watchFiles:!0,enableMemoryMonitoring:!0},templating:{preRender:!1,ssr:!1,ssrPriority:!1,globals:{},state:{},maxFragmentDepth:10}};
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import http from"http";import router from"./router.js";import getFlags from"./getFlags.js";const flags=getFlags(process.argv.slice(2),{port:3e3,logging:2,root:"./",config:".config.
|
|
2
|
+
import http from"http";import router from"./router.js";import getFlags from"./getFlags.js";const flags=getFlags(process.argv.slice(2),{port:3e3,logging:2,root:"./",config:".config.js"},{p:"port",l:"logging",r:"root",c:"config"});if("string"==typeof flags.logging)switch(flags.logging.toLowerCase()){case"silent":flags.logging=0;break;case"minimal":flags.logging=1;break;case"verbose":flags.logging=3;break;case"debug":flags.logging=4;break;default:flags.logging=2}const log=(message,level=2)=>{level<=flags.logging&&console.log(message)};http.createServer(await router(flags,log)).listen(flags.port),log(`Server started at: http://localhost:${flags.port}`);
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{readFile}from"fs/promises";import{join,resolve,dirname}from"path";import{pathToFileURL,fileURLToPath}from"url";import{renderDir}from"./templating/index.js";const args=process.argv.slice(2),inputDir=args[0];inputDir||(console.error("Usage: kempo-render <inputDir> [outputDir] [stateFile]"),process.exit(1));const outputDir=args[1]||inputDir,stateFile=args[2],resolvedInput=resolve(inputDir),resolvedOutput=resolve(outputDir);let globals={},state={},maxDepth=10;const config=await(async dir=>{const jsPath=join(dir,".config.js");try{return(await import(pathToFileURL(jsPath).href)).default}catch(e){try{const json=await readFile(join(dir,".config.json"),"utf8");return JSON.parse(json)}catch(e2){return null}}})(resolvedInput);if(config?.templating&&(globals=config.templating.globals||{},state=config.templating.state||{},maxDepth=config.templating.maxFragmentDepth||10),stateFile){const resolvedState=resolve(stateFile);if(resolvedState.endsWith(".js")){const mod=await import(pathToFileURL(resolvedState).href);state={...state,...mod.default}}else{const json=await readFile(resolvedState,"utf8");state={...state,...JSON.parse(json)}}}const count=await renderDir(resolvedInput,resolvedOutput,globals,state,maxDepth);console.log(`Rendered ${count} page${1!==count?"s":""}`);
|
package/dist/router.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import path from"path";import{readFile,stat,readdir}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 createRequestWrapper,{readRawBody,parseBody}from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";import{onRescan}from"./rescan.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),onRescan(async done=>{try{files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2),done(null,files.length)}catch(error){log(`Rescan failed: ${error.message}`,1),done(error)}});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.isAbsolute(middlewarePath)?middlewarePath:path.resolve(rootPath,middlewarePath),middlewareUrl=pathToFileURL(resolvedPath).href+`?t=${Date.now()}`,customMiddleware=(await import(middlewareUrl)).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)},serveStaticCustomFile=async(filePath,res)=>{const fileExtension=path.extname(filePath).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(filePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;res.writeHead(200,{"Content-Type":contentType}),res.end(fileContent)},executeRouteModule=async(filePath,req,res,params={})=>{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(filePath);if(module=moduleCache.get(filePath,fileStats),!module){const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(filePath,module,fileStats,estimatedSizeKB)}}else{const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl)}if("function"!=typeof module.default)return log(`Route file does not export a function: ${filePath}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Route file does not export a function");const enhancedReq=createRequestWrapper(req,params),enhancedRes=createResponseWrapper(res),rawBody=await readRawBody(req);enhancedReq._rawBody=rawBody,enhancedReq.body=parseBody(rawBody,req.headers["content-type"]),moduleCache&&(enhancedReq._kempoCache=moduleCache),await module.default(enhancedReq,enhancedRes)},walkDynamic=async(base,segments)=>{if(0===segments.length)return{filePath:base,params:{}};const[head,...rest]=segments;let entries;try{entries=await readdir(base,{withFileTypes:!0})}catch{return null}for(const entry of entries)if(entry.name===head)if(entry.isDirectory()){const result=await walkDynamic(path.join(base,head),rest);if(result)return result}else if(entry.isFile()&&0===rest.length)return{filePath:path.join(base,head),params:{}};for(const entry of entries){if(!entry.isDirectory()||!entry.name.startsWith("[")||!entry.name.endsWith("]"))continue;const paramName=entry.name.slice(1,-1),result=await walkDynamic(path.join(base,entry.name),rest);if(result)return{filePath:result.filePath,params:{[paramName]:head,...result.params}}}return null},serveResolvedPath=async(filePath,fileStat,params,req,res)=>{if(fileStat.isDirectory()){const methodUpper=req.method.toUpperCase(),candidates=[`${methodUpper}.js`,`${methodUpper}.html`,"index.js","index.html","index.htm"];for(const candidate of candidates){const candidatePath=path.join(filePath,candidate);try{await stat(candidatePath)}catch{continue}return config.routeFiles.includes(candidate)?(log(`Executing route file: ${candidatePath}`,2),await executeRouteModule(candidatePath,req,res,params),!0):(log(`Serving index file: ${candidatePath}`,2),await serveStaticCustomFile(candidatePath,res),!0)}return null}const fileName=path.basename(filePath);return config.routeFiles.includes(fileName)?(log(`Executing route file: ${filePath}`,2),await executeRouteModule(filePath,req,res,params),!0):(await serveStaticCustomFile(filePath,res),!0)},serveCustomRoutePath=async(resolvedFilePath,req,res)=>{let fileStat;try{return fileStat=await stat(resolvedFilePath),await serveResolvedPath(resolvedFilePath,fileStat,{},req,res)}catch(e){if("ENOENT"!==e.code)throw e}let current=resolvedFilePath;const remaining=[];for(;current!==path.dirname(current);){remaining.unshift(path.basename(current)),current=path.dirname(current);try{if(!(await stat(current)).isDirectory())break;const result=await walkDynamic(current,remaining);if(!result)return null;const resolvedStat=await stat(result.filePath);return await serveResolvedPath(result.filePath,resolvedStat,result.params,req,res)}catch(e2){if("ENOENT"!==e2.code)throw e2}}return null},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)=>{if(parseInt(req.headers["content-length"]||"0",10)>config.maxBodySize)return res.writeHead(413,{"Content-Type":"text/plain"}),void res.end("Payload Too Large");const rawBody=await new Promise((resolve,reject)=>{if(["GET","HEAD"].includes(req.method)&&!req.headers["content-length"])return resolve("");let body="",size=0;req.on("data",chunk=>{if(size+=chunk.length,size>config.maxBodySize)return req.destroy(),void reject(new Error("Payload Too Large"));body+=chunk.toString()}),req.on("end",()=>resolve(body)),req.on("error",reject)}).catch(err=>{if("Payload Too Large"===err.message)return res.writeHead(413,{"Content-Type":"text/plain"}),res.end("Payload Too Large"),null;throw err});if(null===rawBody)return;req._bufferedBody=rawBody;const enhancedRequest=createRequestWrapper(req,{}),enhancedResponse=createResponseWrapper(res);enhancedRequest._rawBody=rawBody,enhancedRequest.body=parseBody(rawBody,req.headers["content-type"]),await middlewareRunner.run(enhancedRequest,enhancedResponse,async()=>{const requestPath=enhancedRequest.url.split("?")[0];log(`${enhancedRequest.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{if(await serveCustomRoutePath(customFilePath,req,res))return;return log(`Custom route path not found: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}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{if(await serveCustomRoutePath(resolvedFilePath,req,res))return;log(`Wildcard route path not found: ${requestPath}`,2)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),enhancedResponse.writeHead(500,{"Content-Type":"text/plain"}),void enhancedResponse.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(!served&&config.maxRescanAttempts>0&&!shouldSkipRescan(requestPath)){log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))}else served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.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,stat,readdir}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 createRequestWrapper,{readRawBody,parseBody}from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";import{onRescan}from"./rescan.js";import{renderDir}from"./templating/index.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.js",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)}let userConfig;if(log(`Loading config from: ${configPath}`,3),configPath.endsWith(".js")){const configUrl=pathToFileURL(configPath).href+`?t=${Date.now()}`;userConfig=(await import(configUrl)).default}else{const configContent=await readFile(configPath,"utf8");userConfig=JSON.parse(configContent)}if(!userConfig)throw new Error("Config file is empty or has no default export");config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes||{}},middleware:{...defaultConfig.middleware,...userConfig.middleware||{}},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes||{}},cache:{...defaultConfig.cache,...userConfig.cache||{}},templating:{...defaultConfig.templating,...userConfig.templating||{}}},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;const configFileName=flags.config||".config.js";if(configFileName.endsWith(".js"))try{const jsonFallback=configFileName.replace(/\.js$/,".json"),jsonPath=path.isAbsolute(jsonFallback)?jsonFallback:path.join(rootPath,jsonFallback);log(`Trying JSON fallback: ${jsonPath}`,3);const configContent=await readFile(jsonPath,"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||{}},templating:{...defaultConfig.templating,...userConfig.templating||{}}},log("User config loaded from JSON fallback",3)}catch(e2){log("Using default config (no config file found)",3)}else log("Using default config (no config file found)",3)}const dis=new Set(config.disallowedRegex);if(dis.add("^/\\..*"),dis.add("\\.config\\.js$"),dis.add("\\.config\\.json$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,3),config.templating.preRender){const{globals:globals,state:state,maxFragmentDepth:maxFragmentDepth}=config.templating,count=await renderDir(rootPath,rootPath,globals,state,maxFragmentDepth);log(`Pre-rendered ${count} page(s)`,2)}let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,2),onRescan(async done=>{try{files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2),done(null,files.length)}catch(error){log(`Rescan failed: ${error.message}`,1),done(error)}});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.isAbsolute(middlewarePath)?middlewarePath:path.resolve(rootPath,middlewarePath),middlewareUrl=pathToFileURL(resolvedPath).href+`?t=${Date.now()}`,customMiddleware=(await import(middlewareUrl)).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.startsWith("/")?pattern:"/"+pattern).replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},serveStaticCustomFile=async(filePath,res)=>{const fileExtension=path.extname(filePath).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(filePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;res.writeHead(200,{"Content-Type":contentType}),res.end(fileContent)},executeRouteModule=async(filePath,req,res,params={})=>{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(filePath);if(module=moduleCache.get(filePath,fileStats),!module){const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(filePath,module,fileStats,estimatedSizeKB)}}else{const fileUrl=pathToFileURL(filePath).href+`?t=${Date.now()}`;module=await import(fileUrl)}if("function"!=typeof module.default)return log(`Route file does not export a function: ${filePath}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Route file does not export a function");const enhancedReq=createRequestWrapper(req,params),enhancedRes=createResponseWrapper(res),rawBody=await readRawBody(req);enhancedReq._rawBody=rawBody,enhancedReq.body=parseBody(rawBody,req.headers["content-type"]),moduleCache&&(enhancedReq._kempoCache=moduleCache),await module.default(enhancedReq,enhancedRes)},walkDynamic=async(base,segments)=>{if(0===segments.length)return{filePath:base,params:{}};const[head,...rest]=segments;let entries;try{entries=await readdir(base,{withFileTypes:!0})}catch{return null}for(const entry of entries)if(entry.name===head)if(entry.isDirectory()){const result=await walkDynamic(path.join(base,head),rest);if(result)return result}else if(entry.isFile()&&0===rest.length)return{filePath:path.join(base,head),params:{}};for(const entry of entries){if(!entry.isDirectory()||!entry.name.startsWith("[")||!entry.name.endsWith("]"))continue;const paramName=entry.name.slice(1,-1),result=await walkDynamic(path.join(base,entry.name),rest);if(result)return{filePath:result.filePath,params:{[paramName]:head,...result.params}}}return null},serveResolvedPath=async(filePath,fileStat,params,req,res)=>{if(fileStat.isDirectory()){const methodUpper=req.method.toUpperCase(),candidates=[`${methodUpper}.js`,`${methodUpper}.html`,"index.js","index.html","index.htm"];for(const candidate of candidates){const candidatePath=path.join(filePath,candidate);try{await stat(candidatePath)}catch{continue}return config.routeFiles.includes(candidate)?(log(`Executing route file: ${candidatePath}`,2),await executeRouteModule(candidatePath,req,res,params),!0):(log(`Serving index file: ${candidatePath}`,2),await serveStaticCustomFile(candidatePath,res),!0)}return null}const fileName=path.basename(filePath);return config.routeFiles.includes(fileName)?(log(`Executing route file: ${filePath}`,2),await executeRouteModule(filePath,req,res,params),!0):(await serveStaticCustomFile(filePath,res),!0)},serveCustomRoutePath=async(resolvedFilePath,req,res)=>{let fileStat;try{return fileStat=await stat(resolvedFilePath),await serveResolvedPath(resolvedFilePath,fileStat,{},req,res)}catch(e){if("ENOENT"!==e.code)throw e}let current=resolvedFilePath;const remaining=[];for(;current!==path.dirname(current);){remaining.unshift(path.basename(current)),current=path.dirname(current);try{if(!(await stat(current)).isDirectory())break;const result=await walkDynamic(current,remaining);if(!result)return null;const resolvedStat=await stat(result.filePath);return await serveResolvedPath(result.filePath,resolvedStat,result.params,req,res)}catch(e2){if("ENOENT"!==e2.code)throw e2}}return null},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)=>{if(parseInt(req.headers["content-length"]||"0",10)>config.maxBodySize)return res.writeHead(413,{"Content-Type":"text/plain"}),void res.end("Payload Too Large");const rawBody=await new Promise((resolve,reject)=>{if(["GET","HEAD"].includes(req.method)&&!req.headers["content-length"])return resolve("");let body="",size=0;req.on("data",chunk=>{if(size+=chunk.length,size>config.maxBodySize)return req.destroy(),void reject(new Error("Payload Too Large"));body+=chunk.toString()}),req.on("end",()=>resolve(body)),req.on("error",reject)}).catch(err=>{if("Payload Too Large"===err.message)return res.writeHead(413,{"Content-Type":"text/plain"}),res.end("Payload Too Large"),null;throw err});if(null===rawBody)return;req._bufferedBody=rawBody;const enhancedRequest=createRequestWrapper(req,{}),enhancedResponse=createResponseWrapper(res);enhancedRequest._rawBody=rawBody,enhancedRequest.body=parseBody(rawBody,req.headers["content-type"]),await middlewareRunner.run(enhancedRequest,enhancedResponse,async()=>{const requestPath=enhancedRequest.url.split("?")[0];log(`${enhancedRequest.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{if(await serveCustomRoutePath(customFilePath,req,res))return;return log(`Custom route path not found: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}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{if(await serveCustomRoutePath(resolvedFilePath,req,res))return;log(`Wildcard route path not found: ${requestPath}`,2)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),enhancedResponse.writeHead(500,{"Content-Type":"text/plain"}),void enhancedResponse.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(!served&&config.maxRescanAttempts>0&&!shouldSkipRescan(requestPath)){log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.end("Not Found"))}else served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),enhancedResponse.writeHead(404,{"Content-Type":"text/plain"}),enhancedResponse.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,{readRawBody,parseBody}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
|
|
1
|
+
import path from"path";import{readFile,stat}from"fs/promises";import{pathToFileURL}from"url";import findFile from"./findFile.js";import createRequestWrapper,{readRawBody,parseBody}from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";import{renderPage}from"./templating/index.js";const trySSR=async(rootPath,requestPath,config,res,log)=>{const htmlPath=requestPath.endsWith("/")?requestPath+"index":requestPath,pagePath=path.join(rootPath,htmlPath.replace(/\.html$/,"")+".page.html");try{await stat(pagePath);const{globals:globals,state:state,maxFragmentDepth:maxFragmentDepth}=config.templating,html=await renderPage(pagePath,rootPath,globals,state,maxFragmentDepth);return res.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),res.end(html),log(`SSR rendered: ${pagePath}`,2),!0}catch(e){return log(`SSR error for ${requestPath}: ${e.message}`,3),!1}};export default async(files,rootPath,requestPath,method,config,req,res,log,moduleCache=null)=>{if(log(`Attempting to serve: ${requestPath}`,3),config.templating?.ssr&&config.templating?.ssrPriority&&await trySSR(rootPath,requestPath,config,res,log))return!0;const[file,params]=await findFile(files,rootPath,requestPath,method,log);if(!file){if(config.templating?.ssr){if(await trySSR(rootPath,requestPath,config,res,log))return!0;log(`SSR fallback not available for: ${requestPath}`,3)}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),rawBody=await readRawBody(req);return enhancedRequest._rawBody=rawBody,enhancedRequest.body=parseBody(rawBody,req.headers["content-type"]),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}}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFile,writeFile,mkdir,readdir}from"fs/promises";import path from"path";import{extractAttrs,extractContentBlocks,replaceLocations,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags}from"./parse.js";import{readFileSync,statSync}from"fs";const findFileUpSync=(filename,startDir,rootDir)=>{let dir=startDir;const root=path.resolve(rootDir);for(;;){const candidate=path.join(dir,filename);try{return statSync(candidate),candidate}catch(e){}if(path.resolve(dir)===root)return null;const parent=path.dirname(dir);if(parent===dir)return null;dir=parent}},loadVersion=rootDir=>{try{return JSON.parse(readFileSync(path.join(rootDir,"package.json"),"utf8")).version||""}catch(e){return""}},renderPage=async(pageFilePath,rootDir,globals={},state={},maxDepth=10)=>{const pageContent=await readFile(pageFilePath,"utf8"),pageTagMatch=pageContent.match(/^[\s\S]*?<page\s([^>]*)>/);if(!pageTagMatch)throw new Error(`Invalid page file: missing <page> root element in ${pageFilePath}`);const pageAttrs=extractAttrs(pageTagMatch[1]),templateName=pageAttrs.template||"default";delete pageAttrs.template;const contentBlocks=extractContentBlocks(pageContent),pageDir=path.dirname(pageFilePath),templateFile=findFileUpSync(`${templateName}.template.html`,pageDir,rootDir);if(!templateFile)throw new Error(`Template not found: ${templateName}.template.html (searched from ${pageDir} to ${rootDir})`);let templateHtml=readFileSync(templateFile,"utf8");templateHtml=resolveFragmentTags(templateHtml,name=>{const filePath=findFileUpSync(name+".fragment.html",pageDir,rootDir);return filePath?readFileSync(filePath,"utf8"):null},0,maxDepth),templateHtml=replaceLocations(templateHtml,contentBlocks);const rel=path.relative(rootDir,path.dirname(pageFilePath)),depth=rel?rel.split(path.sep).length:0,now=new Date,vars={pathToRoot:depth>0?"../".repeat(depth):"./",year:String(now.getFullYear()),date:now.toISOString().slice(0,10),datetime:now.toISOString(),timestamp:String(Date.now()),version:loadVersion(rootDir),env:process.env.NODE_ENV||"",...globals,...state,...pageAttrs};for(const[key,val]of Object.entries(vars))"function"==typeof val&&(vars[key]=val());return templateHtml=resolveIfs(templateHtml,vars),templateHtml=resolveForeach(templateHtml,vars),templateHtml=resolveVars(templateHtml,vars),templateHtml},walkPages=async dir=>{const entries=await readdir(dir,{withFileTypes:!0}),results=[];for(const entry of entries){const full=path.join(dir,entry.name);entry.isDirectory()?results.push(...await walkPages(full)):entry.name.endsWith(".page.html")&&results.push(full)}return results},renderDir=async(inputDir,outputDir,globals={},state={},maxDepth=10)=>{const pages=await walkPages(inputDir);let count=0;for(const page of pages){const outRel=path.relative(inputDir,page).replace(/\.page\.html$/,".html"),outPath=path.join(outputDir,outRel);await mkdir(path.dirname(outPath),{recursive:!0});const html=await renderPage(page,inputDir,globals,state,maxDepth);await writeFile(outPath,html,"utf8"),count++}return count};export{renderPage,renderDir};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const extractAttrs=tagString=>{const attrs={},re=/(\w[\w-]*)=(?:"([^"]*)"|'([^']*)')/g;let match;for(;null!==(match=re.exec(tagString));)attrs[match[1]]=match[2]??match[3];return attrs},extractContentBlocks=xml=>{const blocks={},re=/<content(?:\s+location="([^"]*)")?\s*>([\s\S]*?)<\/content>/g;let match;for(;null!==(match=re.exec(xml));){const name=match[1]||"default";blocks[name]=(blocks[name]||"")+match[2]}return blocks},replaceLocations=(html,contentMap)=>html.replace(/<location(?:\s+name="([^"]*)")?>([\s\S]*?)<\/location>/g,(_,name,fallback)=>contentMap[name||"default"]??fallback).replace(/<location(?:\s+name="([^"]*)")?\s*\/>/g,(_,name)=>contentMap[name||"default"]??""),stripFragmentWrapper=xml=>{const match=xml.match(/^\s*<fragment\b[^>]*>([\s\S]*)<\/fragment>\s*$/);return match?match[1]:xml},resolvePath=(obj,dotPath)=>dotPath.split(".").reduce((cur,key)=>cur?.[key],obj),resolveVars=(html,vars)=>html.replace(/\{\{([^}]+)\}\}/g,(_,key)=>{const trimmed=key.trim(),val=resolvePath(vars,trimmed);return"function"==typeof val?val():val??""}),resolveIfs=(html,vars)=>{const re=/<if\s+condition="([^"]+)">([\s\S]*?)<\/if>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,condition,inner)=>evalCondition(condition,vars)?inner:"")}while(result!==prev);return result},resolveForeach=(html,vars)=>{const re=/<foreach\s+in="([^"]+)"\s+as="([^"]+)">([\s\S]*?)<\/foreach>/g;let prev,result=html;do{prev=result,result=result.replace(re,(_,inAttr,asAttr,inner)=>{const arr=resolvePath(vars,inAttr.trim());return Array.isArray(arr)?arr.map(item=>{const scopedVars={...vars,[asAttr]:item};return resolveVars(inner,scopedVars)}).join(""):""})}while(result!==prev);return result},resolveFragmentTags=(html,findFragmentFile,depth,maxDepth)=>{if(depth>maxDepth)throw new Error(`Fragment depth exceeded maximum of ${maxDepth}`);return html.replace(/<fragment\s+name="([^"]+)"(?:\s*\/>|>([\s\S]*?)<\/fragment>)/g,(_,name,fallback)=>{const content=findFragmentFile(name);if(null===content)return fallback??"";const stripped=stripFragmentWrapper(content);return resolveFragmentTags(stripped,findFragmentFile,depth+1,maxDepth)})},TOKEN_TYPES_NUMBER="NUMBER",TOKEN_TYPES_STRING="STRING",TOKEN_TYPES_BOOLEAN="BOOLEAN",TOKEN_TYPES_IDENTIFIER="IDENTIFIER",TOKEN_TYPES_OPERATOR="OPERATOR",TOKEN_TYPES_NOT="NOT",TOKEN_TYPES_LPAREN="LPAREN",TOKEN_TYPES_RPAREN="RPAREN",evalCondition=(expression,vars)=>!!((tokens,vars)=>{let pos=0;const peek=()=>tokens[pos],advance=()=>tokens[pos++],parsePrimary=()=>{const tok=peek();if(!tok)throw new Error("Unexpected end of expression");if(tok.type===TOKEN_TYPES_NOT)return advance(),!parsePrimary();if(tok.type===TOKEN_TYPES_LPAREN){advance();const val=parseOr();if(!peek()||peek().type!==TOKEN_TYPES_RPAREN)throw new Error("Missing closing parenthesis");return advance(),val}if(tok.type===TOKEN_TYPES_NUMBER||tok.type===TOKEN_TYPES_STRING||tok.type===TOKEN_TYPES_BOOLEAN)return advance(),tok.value;if(tok.type===TOKEN_TYPES_IDENTIFIER)return advance(),resolvePath(vars,tok.value);throw new Error(`Unexpected token: ${JSON.stringify(tok)}`)},parseComparison=()=>{let left=parsePrimary();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&["===","!==",">","<",">=","<="].includes(peek().value);){const op=advance().value,right=parsePrimary();switch(op){case"===":left=left===right;break;case"!==":left=left!==right;break;case">":left=left>right;break;case"<":left=left<right;break;case">=":left=left>=right;break;case"<=":left=left<=right}}return left},parseAnd=()=>{let left=parseComparison();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"&&"===peek().value;){advance();const right=parseComparison();left=left&&right}return left},parseOr=()=>{let left=parseAnd();for(;peek()&&peek().type===TOKEN_TYPES_OPERATOR&&"||"===peek().value;){advance();const right=parseAnd();left=left||right}return left},result=parseOr();if(pos<tokens.length)throw new Error(`Unexpected token after expression: ${JSON.stringify(tokens[pos])}`);return result})((expr=>{const tokens=[];let i=0;for(;i<expr.length;){if(/\s/.test(expr[i])){i++;continue}if("("===expr[i]){tokens.push({type:TOKEN_TYPES_LPAREN}),i++;continue}if(")"===expr[i]){tokens.push({type:TOKEN_TYPES_RPAREN}),i++;continue}if("!"===expr[i]&&"="!==expr[i+1]){tokens.push({type:TOKEN_TYPES_NOT}),i++;continue}const opMatch=expr.slice(i).match(/^(===|!==|>=|<=|&&|\|\||>|<)/);if(opMatch){tokens.push({type:TOKEN_TYPES_OPERATOR,value:opMatch[1]}),i+=opMatch[1].length;continue}if('"'===expr[i]||"'"===expr[i]){const quote=expr[i];let str="";for(i++;i<expr.length&&expr[i]!==quote;)str+=expr[i],i++;if(i>=expr.length)throw new Error(`Unterminated string in condition: ${expr}`);i++,tokens.push({type:TOKEN_TYPES_STRING,value:str});continue}const numMatch=expr.slice(i).match(/^(\d+(\.\d+)?)/);if(numMatch){tokens.push({type:TOKEN_TYPES_NUMBER,value:Number(numMatch[1])}),i+=numMatch[1].length;continue}const idMatch=expr.slice(i).match(/^([a-zA-Z_$][\w$.]*)/);if(idMatch){const id=idMatch[1];"true"===id||"false"===id?tokens.push({type:TOKEN_TYPES_BOOLEAN,value:"true"===id}):tokens.push({type:TOKEN_TYPES_IDENTIFIER,value:id}),i+=id.length;continue}throw new Error(`Unexpected character '${expr[i]}' in condition: ${expr}`)}return tokens})(expression),vars);export{extractAttrs,extractContentBlocks,replaceLocations,stripFragmentWrapper,resolveVars,resolveIfs,resolveForeach,resolveFragmentTags,evalCondition,resolvePath};
|
package/docs/caching.html
CHANGED
|
@@ -1,20 +1,102 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0"
|
|
8
|
+
/>
|
|
9
|
+
<title>Module Caching - Kempo Server</title>
|
|
10
|
+
<link rel="icon" type="image/svg+xml" href="./media/icon.svg" />
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="./media/icon32.png" />
|
|
12
|
+
<link rel="manifest" href="./manifest.json" />
|
|
13
|
+
<link
|
|
14
|
+
rel="stylesheet"
|
|
15
|
+
href="https://cdn.jsdelivr.net/npm/kempo-css@2/dist/kempo.min.css"
|
|
16
|
+
/>
|
|
17
|
+
<link rel="stylesheet" href="./theme.css" />
|
|
18
|
+
<script>
|
|
19
|
+
window.litDisableBundleWarning = true;
|
|
20
|
+
window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
|
|
21
|
+
</script>
|
|
13
22
|
</head>
|
|
14
23
|
<body>
|
|
15
|
-
|
|
16
|
-
<k-
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
<k-nav
|
|
26
|
+
fixed
|
|
27
|
+
class="bg-primary"
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
id="toggleNavSideMenu"
|
|
31
|
+
class="link"
|
|
32
|
+
>
|
|
33
|
+
<k-icon name="menu"></k-icon>
|
|
34
|
+
</button>
|
|
35
|
+
<a
|
|
36
|
+
href="./"
|
|
37
|
+
class="d-if ph"
|
|
38
|
+
style="align-items: center"
|
|
39
|
+
>
|
|
40
|
+
<img src="./media/icon32.png" alt="Kempo Server Icon" class="pr" />
|
|
41
|
+
Kempo Server
|
|
42
|
+
</a>
|
|
43
|
+
<div class="flex"></div>
|
|
44
|
+
<a href="https://github.com/dustinpoissant/kempo-ui?tab=License-1-ov-file#creative-commons-attribution-noncommercial-sharealike-20" target="_blank"><k-icon name="license"></k-icont></a>
|
|
45
|
+
<a href="https://github.com/dustinpoissant/kempo-ui" target="_blank"><k-icon name="github-mark"></k-icont></a>
|
|
46
|
+
<k-theme-switcher></k-theme-switcher>
|
|
47
|
+
</k-nav>
|
|
48
|
+
<div style="width: 100%; height: 4rem;"></div>
|
|
49
|
+
<k-aside
|
|
50
|
+
id="navSideMenu"
|
|
51
|
+
state="offscreen"
|
|
52
|
+
>
|
|
53
|
+
<menu>
|
|
54
|
+
<a href="./" class="ta-center bb mb r0">
|
|
55
|
+
<h1 class="tc-primary">Kempo Server</h1>
|
|
56
|
+
<img src="./media/icon128.png" alt="Kempo UI Icon" />
|
|
57
|
+
</a>
|
|
58
|
+
<h3>Getting Started</h3>
|
|
59
|
+
<a href="./" class="d-b pq pl">Quick Start</a>
|
|
60
|
+
<a href="./routing.html" class="d-b pq pl">Routing</a>
|
|
61
|
+
<a href="./request-response.html" class="d-b pq pl">Request & Response</a>
|
|
62
|
+
<br /><br />
|
|
63
|
+
<h3>Advanced Features</h3>
|
|
64
|
+
<a href="configuration.html" class="d-b pq pl">Configuration</a>
|
|
65
|
+
<a href="templating.html" class="d-b pq pl">Templating</a>
|
|
66
|
+
<a href="middleware.html" class="d-b pq pl">Middleware</a>
|
|
67
|
+
<a href="caching.html" class="d-b pq pl">Module Caching</a>
|
|
68
|
+
<a href="cli-utils.html" class="d-b pq pl">CLI Utilities</a>
|
|
69
|
+
<a href="fs-utils.html" class="d-b pq pl">File System Utilities</a>
|
|
70
|
+
<a href="examples.html" class="d-b pq pl">Examples & Demos</a>
|
|
71
|
+
</menu>
|
|
72
|
+
</k-aside>
|
|
73
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
|
|
74
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
|
|
75
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
|
|
77
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
|
|
78
|
+
<script>
|
|
79
|
+
document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
|
|
80
|
+
await window.customElements.whenDefined('k-aside');
|
|
81
|
+
document.getElementById('navSideMenu').toggle();
|
|
82
|
+
});
|
|
83
|
+
document.addEventListener('click', function(e) {
|
|
84
|
+
if (e.target.matches('a[href^="#"]')) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const targetId = e.target.getAttribute('href').replace('#', '');
|
|
87
|
+
const target = document.getElementById(targetId);
|
|
88
|
+
if (target) {
|
|
89
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
90
|
+
const url = window.location.pathname + window.location.search + '#' + targetId;
|
|
91
|
+
history.replaceState(null, '', url);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<main>
|
|
98
|
+
<h1 class="ta-center">Module Caching</h1>
|
|
99
|
+
|
|
18
100
|
<p>Kempo Server includes an intelligent module caching system that dramatically improves performance by caching JavaScript route modules in memory.</p>
|
|
19
101
|
|
|
20
102
|
<h2>How It Works</h2>
|
|
@@ -231,8 +313,12 @@
|
|
|
231
313
|
<li>Extend <code>ttlMs</code> for stable route files</li>
|
|
232
314
|
<li>Monitor hit rates with admin endpoints</li>
|
|
233
315
|
</ul>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
316
|
+
|
|
317
|
+
</main>
|
|
318
|
+
<div style="height:25vh"></div>
|
|
319
|
+
<script
|
|
320
|
+
type="module"
|
|
321
|
+
src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
|
|
322
|
+
></script>
|
|
237
323
|
</body>
|
|
238
|
-
</html>
|
|
324
|
+
</html>
|
package/docs/cli-utils.html
CHANGED
|
@@ -1,20 +1,102 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta
|
|
6
|
+
name="viewport"
|
|
7
|
+
content="width=device-width, initial-scale=1.0"
|
|
8
|
+
/>
|
|
9
|
+
<title>CLI Utilities - Kempo Server</title>
|
|
10
|
+
<link rel="icon" type="image/svg+xml" href="./media/icon.svg" />
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="./media/icon32.png" />
|
|
12
|
+
<link rel="manifest" href="./manifest.json" />
|
|
13
|
+
<link
|
|
14
|
+
rel="stylesheet"
|
|
15
|
+
href="https://cdn.jsdelivr.net/npm/kempo-css@2/dist/kempo.min.css"
|
|
16
|
+
/>
|
|
17
|
+
<link rel="stylesheet" href="./theme.css" />
|
|
18
|
+
<script>
|
|
19
|
+
window.litDisableBundleWarning = true;
|
|
20
|
+
window.kempo = { pathsToIcons: ['https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/icons/'] };
|
|
21
|
+
</script>
|
|
13
22
|
</head>
|
|
14
23
|
<body>
|
|
15
|
-
|
|
16
|
-
<k-
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
<k-nav
|
|
26
|
+
fixed
|
|
27
|
+
class="bg-primary"
|
|
28
|
+
>
|
|
29
|
+
<button
|
|
30
|
+
id="toggleNavSideMenu"
|
|
31
|
+
class="link"
|
|
32
|
+
>
|
|
33
|
+
<k-icon name="menu"></k-icon>
|
|
34
|
+
</button>
|
|
35
|
+
<a
|
|
36
|
+
href="./"
|
|
37
|
+
class="d-if ph"
|
|
38
|
+
style="align-items: center"
|
|
39
|
+
>
|
|
40
|
+
<img src="./media/icon32.png" alt="Kempo Server Icon" class="pr" />
|
|
41
|
+
Kempo Server
|
|
42
|
+
</a>
|
|
43
|
+
<div class="flex"></div>
|
|
44
|
+
<a href="https://github.com/dustinpoissant/kempo-ui?tab=License-1-ov-file#creative-commons-attribution-noncommercial-sharealike-20" target="_blank"><k-icon name="license"></k-icont></a>
|
|
45
|
+
<a href="https://github.com/dustinpoissant/kempo-ui" target="_blank"><k-icon name="github-mark"></k-icont></a>
|
|
46
|
+
<k-theme-switcher></k-theme-switcher>
|
|
47
|
+
</k-nav>
|
|
48
|
+
<div style="width: 100%; height: 4rem;"></div>
|
|
49
|
+
<k-aside
|
|
50
|
+
id="navSideMenu"
|
|
51
|
+
state="offscreen"
|
|
52
|
+
>
|
|
53
|
+
<menu>
|
|
54
|
+
<a href="./" class="ta-center bb mb r0">
|
|
55
|
+
<h1 class="tc-primary">Kempo Server</h1>
|
|
56
|
+
<img src="./media/icon128.png" alt="Kempo UI Icon" />
|
|
57
|
+
</a>
|
|
58
|
+
<h3>Getting Started</h3>
|
|
59
|
+
<a href="./" class="d-b pq pl">Quick Start</a>
|
|
60
|
+
<a href="./routing.html" class="d-b pq pl">Routing</a>
|
|
61
|
+
<a href="./request-response.html" class="d-b pq pl">Request & Response</a>
|
|
62
|
+
<br /><br />
|
|
63
|
+
<h3>Advanced Features</h3>
|
|
64
|
+
<a href="configuration.html" class="d-b pq pl">Configuration</a>
|
|
65
|
+
<a href="templating.html" class="d-b pq pl">Templating</a>
|
|
66
|
+
<a href="middleware.html" class="d-b pq pl">Middleware</a>
|
|
67
|
+
<a href="caching.html" class="d-b pq pl">Module Caching</a>
|
|
68
|
+
<a href="cli-utils.html" class="d-b pq pl">CLI Utilities</a>
|
|
69
|
+
<a href="fs-utils.html" class="d-b pq pl">File System Utilities</a>
|
|
70
|
+
<a href="examples.html" class="d-b pq pl">Examples & Demos</a>
|
|
71
|
+
</menu>
|
|
72
|
+
</k-aside>
|
|
73
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Aside.js" type="module"></script>
|
|
74
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Main.js" type="module"></script>
|
|
75
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Nav.js" type="module"></script>
|
|
76
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/Icon.js" type="module"></script>
|
|
77
|
+
<script src="https://cdn.jsdelivr.net/npm/kempo-ui@0.3.5/src/components/ThemeSwitcher.js" type="module"></script>
|
|
78
|
+
<script>
|
|
79
|
+
document.getElementById('toggleNavSideMenu').addEventListener('click', async () => {
|
|
80
|
+
await window.customElements.whenDefined('k-aside');
|
|
81
|
+
document.getElementById('navSideMenu').toggle();
|
|
82
|
+
});
|
|
83
|
+
document.addEventListener('click', function(e) {
|
|
84
|
+
if (e.target.matches('a[href^="#"]')) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const targetId = e.target.getAttribute('href').replace('#', '');
|
|
87
|
+
const target = document.getElementById(targetId);
|
|
88
|
+
if (target) {
|
|
89
|
+
target.scrollIntoView({ behavior: 'smooth' });
|
|
90
|
+
const url = window.location.pathname + window.location.search + '#' + targetId;
|
|
91
|
+
history.replaceState(null, '', url);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<main>
|
|
98
|
+
<h1 class="ta-center">CLI Utilities</h1>
|
|
99
|
+
|
|
18
100
|
<p>The CLI utilities provide simple command-line argument parsing functionality for Node.js applications.</p>
|
|
19
101
|
|
|
20
102
|
<h2>Installation</h2>
|
|
@@ -82,8 +164,12 @@
|
|
|
82
164
|
<li><code>promptUser(query)</code> - Prompt user for input</li>
|
|
83
165
|
<li><code>promptYN(query, defaultValue)</code> - Prompt for yes/no with default</li>
|
|
84
166
|
</ul>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
|
|
168
|
+
</main>
|
|
169
|
+
<div style="height:25vh"></div>
|
|
170
|
+
<script
|
|
171
|
+
type="module"
|
|
172
|
+
src="https://cdn.jsdelivr.net/npm/kempo-ui@0.0.42/dist/components/Import.js"
|
|
173
|
+
></script>
|
|
88
174
|
</body>
|
|
89
175
|
</html>
|