kempo-server 1.7.3 โ 1.7.5
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 +501 -0
- package/README.md +13 -442
- package/UTILS.md +127 -0
- package/dist/defaultConfig.js +1 -1
- package/dist/moduleCache.js +1 -0
- package/dist/router.js +1 -1
- package/dist/serveFile.js +1 -1
- package/dist/utils/cli.js +1 -0
- package/dist/utils/fs-utils.js +1 -0
- package/package.json +7 -2
- package/scripts/build.js +57 -43
- package/tests/router-wildcard-double-asterisk.node-test.js +66 -0
- package/config-examples/development.config.json +0 -24
- package/config-examples/low-memory.config.json +0 -23
- package/config-examples/no-cache.config.json +0 -13
- package/config-examples/production.config.json +0 -38
- package/example-cache.config.json +0 -45
- package/example.config.json +0 -50
- package/utils/cache-demo.js +0 -145
- package/utils/cache-monitor.js +0 -132
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn}from"child_process";import readline from"readline";export const getArgs=(mapping={})=>{const args={};let name="",values=[];const save=()=>{name&&(0===values.length?args[name]=!0:1===values.length?"false"===values[0]?args[name]=!1:args[name]=values[0]:args[name]=values)};for(let i=2;i<process.argv.length;i++){const arg=process.argv[i];arg.startsWith("-")?(save(),arg.startsWith("--")?name=arg.slice(2):(name=arg.slice(1),mapping[name]&&(name=mapping[name])),values=[]):values.push(arg)}return save(),args};export const runChildProcess=command=>new Promise((resolve,reject)=>{const[cmd,...args]=command.split(" "),child=spawn(cmd,args,{stdio:"inherit",shell:!0});child.on("close",code=>{0===code?resolve(`child process exited with code ${code}`):reject(new Error(`child process exited with code ${code}`))}),child.on("error",reject)});export const runChildNodeProcess=(scriptPath,argsObj={})=>{const command=`node ${scriptPath} ${Object.entries(argsObj).flatMap(([key,value])=>!0===value?[`--${key}`]:[`--${key}`,value]).join(" ")}`;return runChildProcess(command)};export const runChildNodeProcessScript=scriptPath=>{const child=spawn("node",[scriptPath],{stdio:"inherit",shell:!0});return new Promise((resolve,reject)=>{child.on("close",code=>{0===code?resolve():reject(new Error(`Process exited with code ${code}`))}),child.on("error",reject)})};export const promptUser=query=>{const rl=readline.createInterface({input:process.stdin,output:process.stdout});return new Promise(resolve=>{rl.question(`${query}: `,answer=>{rl.close(),resolve(answer)})})};export const promptYN=(query,defaultValue="y")=>promptUser(`${query} (${"y"===defaultValue?"Y/n":"y/N"}): `).then(answer=>{const normalizedAnswer=answer.trim().toLowerCase();return""===normalizedAnswer?"y"===defaultValue:"y"===normalizedAnswer});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import fs from"fs/promises";import path from"path";export const ensureDir=async dirPath=>{try{await fs.mkdir(dirPath,{recursive:!0})}catch(error){if("EEXIST"!==error.code)throw error}};export const copyDir=async(src,dest)=>{await ensureDir(dest);const entries=await fs.readdir(src,{withFileTypes:!0});for(const entry of entries){const srcPath=path.join(src,entry.name),destPath=path.join(dest,entry.name);entry.isDirectory()?await copyDir(srcPath,destPath):await fs.copyFile(srcPath,destPath)}};export const emptyDir=async dirPath=>{try{const entries=await fs.readdir(dirPath);await Promise.all(entries.map(entry=>fs.rm(path.join(dirPath,entry),{recursive:!0,force:!0})))}catch(error){if("ENOENT"!==error.code)throw error}};
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.5",
|
|
5
5
|
"description": "A lightweight, zero-dependency, file based routing server.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./utils/cli": "./dist/utils/cli.js",
|
|
10
|
+
"./utils/fs-utils": "./dist/utils/fs-utils.js"
|
|
11
|
+
},
|
|
7
12
|
"bin": {
|
|
8
13
|
"kempo-server": "./dist/index.js"
|
|
9
14
|
},
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "node scripts/build.js",
|
|
12
|
-
"
|
|
17
|
+
"docs": "node dist/index.js -r ./docs",
|
|
13
18
|
"tests": "npx kempo-test",
|
|
14
19
|
"tests:gui": "npx kempo-test --gui",
|
|
15
20
|
"tests:browser": "npx kempo-test -b",
|
package/scripts/build.js
CHANGED
|
@@ -6,9 +6,52 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const rootDir = join(__dirname, '..');
|
|
8
8
|
const srcDir = join(rootDir, 'src');
|
|
9
|
+
const utilsDir = join(rootDir, 'utils');
|
|
9
10
|
const distDir = join(rootDir, 'dist');
|
|
10
11
|
const docsDir = join(rootDir, 'docs');
|
|
11
12
|
|
|
13
|
+
/*
|
|
14
|
+
File Processing Helper
|
|
15
|
+
*/
|
|
16
|
+
const processJsFile = async (srcPath, distPath) => {
|
|
17
|
+
const fileName = distPath.split(/[/\\]/).pop();
|
|
18
|
+
console.log(`Minifying ${fileName}...`);
|
|
19
|
+
|
|
20
|
+
// Read source file
|
|
21
|
+
let sourceCode = await readFile(srcPath, 'utf8');
|
|
22
|
+
|
|
23
|
+
// Check for and preserve shebang
|
|
24
|
+
let shebang = '';
|
|
25
|
+
if (sourceCode.startsWith('#!')) {
|
|
26
|
+
const firstNewline = sourceCode.indexOf('\n');
|
|
27
|
+
shebang = sourceCode.substring(0, firstNewline + 1);
|
|
28
|
+
sourceCode = sourceCode.substring(firstNewline + 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Minify with Terser
|
|
32
|
+
const result = await minify(sourceCode, {
|
|
33
|
+
module: true, // Enable ES module support
|
|
34
|
+
format: {
|
|
35
|
+
comments: false,
|
|
36
|
+
},
|
|
37
|
+
compress: {
|
|
38
|
+
dead_code: true,
|
|
39
|
+
drop_console: false,
|
|
40
|
+
drop_debugger: true,
|
|
41
|
+
},
|
|
42
|
+
mangle: false, // Keep function names for better debugging
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result.error) {
|
|
46
|
+
throw new Error(`Minification failed for ${fileName}: ${result.error}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Write minified file to dist with shebang if it existed
|
|
50
|
+
const finalCode = shebang + result.code;
|
|
51
|
+
await writeFile(distPath, finalCode);
|
|
52
|
+
console.log(`โ Built ${fileName}`);
|
|
53
|
+
};
|
|
54
|
+
|
|
12
55
|
/*
|
|
13
56
|
Build Process
|
|
14
57
|
*/
|
|
@@ -16,53 +59,24 @@ const build = async () => {
|
|
|
16
59
|
try {
|
|
17
60
|
// Ensure dist directory exists
|
|
18
61
|
await mkdir(distDir, { recursive: true });
|
|
62
|
+
await mkdir(join(distDir, 'utils'), { recursive: true });
|
|
19
63
|
|
|
20
64
|
console.log('Building JavaScript files...');
|
|
21
65
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const
|
|
66
|
+
// Process src directory
|
|
67
|
+
const srcFiles = await readdir(srcDir);
|
|
68
|
+
const srcJsFiles = srcFiles.filter(file => file.endsWith('.js'));
|
|
69
|
+
|
|
70
|
+
for (const file of srcJsFiles) {
|
|
71
|
+
await processJsFile(join(srcDir, file), join(distDir, file));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Process utils directory
|
|
75
|
+
const utilsFiles = await readdir(utilsDir);
|
|
76
|
+
const utilsJsFiles = utilsFiles.filter(file => file.endsWith('.js'));
|
|
25
77
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const srcPath = join(srcDir, file);
|
|
29
|
-
const distPath = join(distDir, file);
|
|
30
|
-
|
|
31
|
-
console.log(`Minifying ${file}...`);
|
|
32
|
-
|
|
33
|
-
// Read source file
|
|
34
|
-
let sourceCode = await readFile(srcPath, 'utf8');
|
|
35
|
-
|
|
36
|
-
// Check for and preserve shebang
|
|
37
|
-
let shebang = '';
|
|
38
|
-
if (sourceCode.startsWith('#!')) {
|
|
39
|
-
const firstNewline = sourceCode.indexOf('\n');
|
|
40
|
-
shebang = sourceCode.substring(0, firstNewline + 1);
|
|
41
|
-
sourceCode = sourceCode.substring(firstNewline + 1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Minify with Terser
|
|
45
|
-
const result = await minify(sourceCode, {
|
|
46
|
-
module: true, // Enable ES module support
|
|
47
|
-
format: {
|
|
48
|
-
comments: false,
|
|
49
|
-
},
|
|
50
|
-
compress: {
|
|
51
|
-
dead_code: true,
|
|
52
|
-
drop_console: false,
|
|
53
|
-
drop_debugger: true,
|
|
54
|
-
},
|
|
55
|
-
mangle: false, // Keep function names for better debugging
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (result.error) {
|
|
59
|
-
throw new Error(`Minification failed for ${file}: ${result.error}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Write minified file to dist with shebang if it existed
|
|
63
|
-
const finalCode = shebang + result.code;
|
|
64
|
-
await writeFile(distPath, finalCode);
|
|
65
|
-
console.log(`โ Built ${file}`);
|
|
78
|
+
for (const file of utilsJsFiles) {
|
|
79
|
+
await processJsFile(join(utilsDir, file), join(distDir, 'utils', file));
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
console.log('Copying kempo.min.css to docs...');
|
|
@@ -169,5 +169,71 @@ export default {
|
|
|
169
169
|
} catch(e){
|
|
170
170
|
fail(e.message);
|
|
171
171
|
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
'wildcard routes serve nested files from server root': async ({pass, fail, log}) => {
|
|
175
|
+
try {
|
|
176
|
+
await withTempDir(async (dir) => {
|
|
177
|
+
// Create nested file structure in src directory
|
|
178
|
+
const importFile = await write(dir, 'src/components/Import.js', 'export default ImportComponent');
|
|
179
|
+
const utilsFile = await write(dir, 'src/utils/helpers.js', 'export const helpers = {}');
|
|
180
|
+
const deepFile = await write(dir, 'src/deep/nested/file.js', 'export const nested = true');
|
|
181
|
+
|
|
182
|
+
// Create index.html in server root
|
|
183
|
+
await write(dir, 'index.html', '<h1>Home</h1>');
|
|
184
|
+
|
|
185
|
+
const prev = process.cwd();
|
|
186
|
+
process.chdir(dir);
|
|
187
|
+
|
|
188
|
+
// Server root is the current directory (dir)
|
|
189
|
+
const flags = {root: '.', logging: 0, scan: false};
|
|
190
|
+
const logFn = () => {};
|
|
191
|
+
|
|
192
|
+
// Configure wildcard route to serve from ./src/**
|
|
193
|
+
const config = {
|
|
194
|
+
customRoutes: {
|
|
195
|
+
'/src/**': './src/**'
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await write(dir, '.config.json', JSON.stringify(config));
|
|
200
|
+
const handler = await router(flags, logFn);
|
|
201
|
+
const server = http.createServer(handler);
|
|
202
|
+
const port = randomPort();
|
|
203
|
+
|
|
204
|
+
await new Promise(r => server.listen(port, r));
|
|
205
|
+
await new Promise(r => setTimeout(r, 50));
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Test that /src/components/Import.js is served correctly
|
|
209
|
+
const r1 = await httpGet(`http://localhost:${port}/src/components/Import.js`);
|
|
210
|
+
log('Import.js status: ' + r1.res.statusCode);
|
|
211
|
+
if(r1.res.statusCode !== 200) throw new Error(`Expected 200, got ${r1.res.statusCode}`);
|
|
212
|
+
if(r1.body.toString() !== 'export default ImportComponent') throw new Error('Import.js content mismatch');
|
|
213
|
+
|
|
214
|
+
// Test deeper nested file
|
|
215
|
+
const r2 = await httpGet(`http://localhost:${port}/src/utils/helpers.js`);
|
|
216
|
+
log('helpers.js status: ' + r2.res.statusCode);
|
|
217
|
+
if(r2.res.statusCode !== 200) throw new Error(`Expected 200 for helpers.js, got ${r2.res.statusCode}`);
|
|
218
|
+
|
|
219
|
+
// Test very deeply nested file
|
|
220
|
+
const r3 = await httpGet(`http://localhost:${port}/src/deep/nested/file.js`);
|
|
221
|
+
log('deep nested file status: ' + r3.res.statusCode);
|
|
222
|
+
if(r3.res.statusCode !== 200) throw new Error(`Expected 200 for deep nested file, got ${r3.res.statusCode}`);
|
|
223
|
+
|
|
224
|
+
// Test that index.html still works (non-wildcard route)
|
|
225
|
+
const r4 = await httpGet(`http://localhost:${port}/index.html`);
|
|
226
|
+
log('index.html status: ' + r4.res.statusCode);
|
|
227
|
+
if(r4.res.statusCode !== 200) throw new Error(`Expected 200 for index.html, got ${r4.res.statusCode}`);
|
|
228
|
+
|
|
229
|
+
} finally {
|
|
230
|
+
server.close();
|
|
231
|
+
process.chdir(prev);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
pass('wildcard routes serve nested files from server root');
|
|
235
|
+
} catch(e){
|
|
236
|
+
fail(e.message);
|
|
237
|
+
}
|
|
172
238
|
}
|
|
173
239
|
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "Development configuration with aggressive file watching and conservative memory usage",
|
|
3
|
-
"cache": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"maxSize": 50,
|
|
6
|
-
"maxMemoryMB": 25,
|
|
7
|
-
"ttlMs": 300000,
|
|
8
|
-
"maxHeapUsagePercent": 65,
|
|
9
|
-
"memoryCheckInterval": 15000,
|
|
10
|
-
"watchFiles": true,
|
|
11
|
-
"enableMemoryMonitoring": true
|
|
12
|
-
},
|
|
13
|
-
"middleware": {
|
|
14
|
-
"cors": {
|
|
15
|
-
"enabled": true,
|
|
16
|
-
"origin": "*"
|
|
17
|
-
},
|
|
18
|
-
"logging": {
|
|
19
|
-
"enabled": true,
|
|
20
|
-
"includeUserAgent": true,
|
|
21
|
-
"includeResponseTime": true
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "Low-memory configuration for resource-constrained environments",
|
|
3
|
-
"cache": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"maxSize": 20,
|
|
6
|
-
"maxMemoryMB": 5,
|
|
7
|
-
"ttlMs": 120000,
|
|
8
|
-
"maxHeapUsagePercent": 60,
|
|
9
|
-
"memoryCheckInterval": 10000,
|
|
10
|
-
"watchFiles": true,
|
|
11
|
-
"enableMemoryMonitoring": true
|
|
12
|
-
},
|
|
13
|
-
"middleware": {
|
|
14
|
-
"compression": {
|
|
15
|
-
"enabled": true,
|
|
16
|
-
"threshold": 512
|
|
17
|
-
},
|
|
18
|
-
"logging": {
|
|
19
|
-
"enabled": true,
|
|
20
|
-
"includeResponseTime": false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "Production configuration with large cache and disabled file watching",
|
|
3
|
-
"cache": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"maxSize": 1000,
|
|
6
|
-
"maxMemoryMB": 200,
|
|
7
|
-
"ttlMs": 3600000,
|
|
8
|
-
"maxHeapUsagePercent": 85,
|
|
9
|
-
"memoryCheckInterval": 60000,
|
|
10
|
-
"watchFiles": false,
|
|
11
|
-
"enableMemoryMonitoring": true
|
|
12
|
-
},
|
|
13
|
-
"middleware": {
|
|
14
|
-
"cors": {
|
|
15
|
-
"enabled": true,
|
|
16
|
-
"origin": ["https://myapp.com", "https://www.myapp.com"],
|
|
17
|
-
"credentials": true
|
|
18
|
-
},
|
|
19
|
-
"compression": {
|
|
20
|
-
"enabled": true,
|
|
21
|
-
"threshold": 1024
|
|
22
|
-
},
|
|
23
|
-
"security": {
|
|
24
|
-
"enabled": true,
|
|
25
|
-
"headers": {
|
|
26
|
-
"X-Content-Type-Options": "nosniff",
|
|
27
|
-
"X-Frame-Options": "DENY",
|
|
28
|
-
"X-XSS-Protection": "1; mode=block",
|
|
29
|
-
"Strict-Transport-Security": "max-age=31536000; includeSubDomains"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"logging": {
|
|
33
|
-
"enabled": true,
|
|
34
|
-
"includeUserAgent": false,
|
|
35
|
-
"includeResponseTime": true
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"cache": {
|
|
3
|
-
"enabled": true,
|
|
4
|
-
"maxSize": 200,
|
|
5
|
-
"maxMemoryMB": 75,
|
|
6
|
-
"ttlMs": 600000,
|
|
7
|
-
"maxHeapUsagePercent": 75,
|
|
8
|
-
"memoryCheckInterval": 15000,
|
|
9
|
-
"watchFiles": true,
|
|
10
|
-
"enableMemoryMonitoring": true,
|
|
11
|
-
"production": {
|
|
12
|
-
"maxSize": 1000,
|
|
13
|
-
"maxMemoryMB": 200,
|
|
14
|
-
"ttlMs": 3600000,
|
|
15
|
-
"maxHeapUsagePercent": 85,
|
|
16
|
-
"memoryCheckInterval": 60000,
|
|
17
|
-
"watchFiles": false
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"middleware": {
|
|
21
|
-
"cors": {
|
|
22
|
-
"enabled": true,
|
|
23
|
-
"origin": "*",
|
|
24
|
-
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
25
|
-
"headers": ["Content-Type", "Authorization"]
|
|
26
|
-
},
|
|
27
|
-
"compression": {
|
|
28
|
-
"enabled": true,
|
|
29
|
-
"threshold": 1024
|
|
30
|
-
},
|
|
31
|
-
"security": {
|
|
32
|
-
"enabled": true,
|
|
33
|
-
"headers": {
|
|
34
|
-
"X-Content-Type-Options": "nosniff",
|
|
35
|
-
"X-Frame-Options": "DENY",
|
|
36
|
-
"X-XSS-Protection": "1; mode=block"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"logging": {
|
|
40
|
-
"enabled": true,
|
|
41
|
-
"includeUserAgent": false,
|
|
42
|
-
"includeResponseTime": true
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
package/example.config.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"allowedMimes": {
|
|
3
|
-
"html": "text/html",
|
|
4
|
-
"css": "text/css",
|
|
5
|
-
"js": "application/javascript",
|
|
6
|
-
"json": "application/json",
|
|
7
|
-
"png": "image/png",
|
|
8
|
-
"jpg": "image/jpeg"
|
|
9
|
-
},
|
|
10
|
-
"middleware": {
|
|
11
|
-
"cors": {
|
|
12
|
-
"enabled": true,
|
|
13
|
-
"origin": ["http://localhost:3000", "https://mydomain.com"],
|
|
14
|
-
"methods": ["GET", "POST", "PUT", "DELETE"],
|
|
15
|
-
"headers": ["Content-Type", "Authorization", "X-API-Key"]
|
|
16
|
-
},
|
|
17
|
-
"compression": {
|
|
18
|
-
"enabled": true,
|
|
19
|
-
"threshold": 512
|
|
20
|
-
},
|
|
21
|
-
"rateLimit": {
|
|
22
|
-
"enabled": true,
|
|
23
|
-
"maxRequests": 50,
|
|
24
|
-
"windowMs": 60000,
|
|
25
|
-
"message": "Rate limit exceeded. Please try again later."
|
|
26
|
-
},
|
|
27
|
-
"security": {
|
|
28
|
-
"enabled": true,
|
|
29
|
-
"headers": {
|
|
30
|
-
"X-Content-Type-Options": "nosniff",
|
|
31
|
-
"X-Frame-Options": "SAMEORIGIN",
|
|
32
|
-
"X-XSS-Protection": "1; mode=block",
|
|
33
|
-
"Strict-Transport-Security": "max-age=31536000; includeSubDomains"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"logging": {
|
|
37
|
-
"enabled": true,
|
|
38
|
-
"includeUserAgent": true,
|
|
39
|
-
"includeResponseTime": true
|
|
40
|
-
},
|
|
41
|
-
"custom": [
|
|
42
|
-
"./middleware/auth.js",
|
|
43
|
-
"./middleware/analytics.js"
|
|
44
|
-
]
|
|
45
|
-
},
|
|
46
|
-
"customRoutes": {
|
|
47
|
-
"api/*": "./api-handlers/*",
|
|
48
|
-
"/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
|
|
49
|
-
}
|
|
50
|
-
}
|
package/utils/cache-demo.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
Cache Demo Script
|
|
5
|
-
Demonstrates the module cache functionality with real-time monitoring
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { writeFile, unlink, mkdir, rm } from 'fs/promises';
|
|
10
|
-
import ModuleCache from '../src/moduleCache.js';
|
|
11
|
-
|
|
12
|
-
const createDemoFiles = async (demoDir) => {
|
|
13
|
-
await mkdir(demoDir, { recursive: true });
|
|
14
|
-
|
|
15
|
-
// Create some demo route files
|
|
16
|
-
await writeFile(path.join(demoDir, 'users.js'),
|
|
17
|
-
'export default async (req, res) => res.json({ users: ["alice", "bob"] });'
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
await writeFile(path.join(demoDir, 'posts.js'),
|
|
21
|
-
'export default async (req, res) => res.json({ posts: ["post1", "post2"] });'
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
await writeFile(path.join(demoDir, 'auth.js'),
|
|
25
|
-
'export default async (req, res) => res.json({ authenticated: true });'
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return [
|
|
29
|
-
path.join(demoDir, 'users.js'),
|
|
30
|
-
path.join(demoDir, 'posts.js'),
|
|
31
|
-
path.join(demoDir, 'auth.js')
|
|
32
|
-
];
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const simulateFileAccess = async (cache, filePaths) => {
|
|
36
|
-
const { stat } = await import('fs/promises');
|
|
37
|
-
|
|
38
|
-
for(const filePath of filePaths) {
|
|
39
|
-
const stats = await stat(filePath);
|
|
40
|
-
|
|
41
|
-
// Try to get from cache first
|
|
42
|
-
let module = cache.get(filePath, stats);
|
|
43
|
-
|
|
44
|
-
if(!module) {
|
|
45
|
-
// Simulate loading module
|
|
46
|
-
const fileUrl = `file://${filePath}?t=${Date.now()}`;
|
|
47
|
-
module = await import(fileUrl);
|
|
48
|
-
cache.set(filePath, module, stats, stats.size / 1024);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
console.log(`๐ ${path.basename(filePath)}: ${module ? 'โ
cached' : 'โ loaded fresh'}`);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const displayCacheStats = (cache) => {
|
|
56
|
-
console.log('\n๐ Cache Statistics:');
|
|
57
|
-
console.log('โ'.repeat(40));
|
|
58
|
-
|
|
59
|
-
const stats = cache.getStats();
|
|
60
|
-
console.log(`Entries: ${stats.cache.size}/${stats.cache.maxSize}`);
|
|
61
|
-
console.log(`Memory: ${stats.cache.memoryUsageMB}/${stats.cache.maxMemoryMB}MB`);
|
|
62
|
-
console.log(`Hit Rate: ${cache.getHitRate()}%`);
|
|
63
|
-
console.log(`Hits: ${stats.stats.hits}, Misses: ${stats.stats.misses}`);
|
|
64
|
-
console.log(`File Changes: ${stats.stats.fileChanges}`);
|
|
65
|
-
console.log(`Heap Usage: ${stats.memory.heapUsedMB}MB (${stats.memory.heapUsagePercent}%)`);
|
|
66
|
-
|
|
67
|
-
if(stats.cache.size > 0) {
|
|
68
|
-
console.log('\n๐ Cached Files:');
|
|
69
|
-
const cachedFiles = cache.getCachedFiles();
|
|
70
|
-
cachedFiles.forEach(file => {
|
|
71
|
-
const ageSeconds = Math.round(file.age / 1000);
|
|
72
|
-
console.log(` โข ${file.relativePath} (${file.sizeKB.toFixed(1)}KB, ${ageSeconds}s old)`);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
console.log();
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const main = async () => {
|
|
79
|
-
console.log('๐ Kempo Server Module Cache Demo\n');
|
|
80
|
-
|
|
81
|
-
// Create demo directory and files
|
|
82
|
-
const demoDir = path.join(process.cwd(), 'demo-cache-test');
|
|
83
|
-
console.log('๐ Creating demo files...');
|
|
84
|
-
const filePaths = await createDemoFiles(demoDir);
|
|
85
|
-
|
|
86
|
-
// Initialize cache with small limits for demo
|
|
87
|
-
const cache = new ModuleCache({
|
|
88
|
-
maxSize: 2,
|
|
89
|
-
maxMemoryMB: 1,
|
|
90
|
-
ttlMs: 5000, // 5 seconds
|
|
91
|
-
watchFiles: true,
|
|
92
|
-
enableMemoryMonitoring: true
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
console.log('โ
Cache initialized\n');
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
// Round 1: Initial loads (cache misses)
|
|
99
|
-
console.log('๐ Round 1: Initial file access (expect cache misses)');
|
|
100
|
-
await simulateFileAccess(cache, filePaths);
|
|
101
|
-
displayCacheStats(cache);
|
|
102
|
-
|
|
103
|
-
// Round 2: Immediate re-access (cache hits)
|
|
104
|
-
console.log('๐ Round 2: Immediate re-access (expect cache hits)');
|
|
105
|
-
await simulateFileAccess(cache, [filePaths[0], filePaths[1]]);
|
|
106
|
-
displayCacheStats(cache);
|
|
107
|
-
|
|
108
|
-
// Round 3: Add third file (should evict oldest due to maxSize=2)
|
|
109
|
-
console.log('๐ Round 3: Access third file (expect LRU eviction)');
|
|
110
|
-
await simulateFileAccess(cache, [filePaths[2]]);
|
|
111
|
-
displayCacheStats(cache);
|
|
112
|
-
|
|
113
|
-
// Round 4: Wait for TTL expiration
|
|
114
|
-
console.log('โฑ๏ธ Waiting 6 seconds for TTL expiration...');
|
|
115
|
-
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
116
|
-
|
|
117
|
-
console.log('๐ Round 4: Access after TTL expiration (expect cache misses)');
|
|
118
|
-
await simulateFileAccess(cache, [filePaths[1]]);
|
|
119
|
-
displayCacheStats(cache);
|
|
120
|
-
|
|
121
|
-
// Round 5: Demonstrate file watching
|
|
122
|
-
console.log('๐ Round 5: Modifying file to test file watching...');
|
|
123
|
-
await writeFile(filePaths[1],
|
|
124
|
-
'export default async (req, res) => res.json({ posts: ["updated post"] });'
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Wait for file watcher to detect change
|
|
128
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
129
|
-
|
|
130
|
-
console.log('๐ Accessing modified file (expect cache invalidation)');
|
|
131
|
-
await simulateFileAccess(cache, [filePaths[1]]);
|
|
132
|
-
displayCacheStats(cache);
|
|
133
|
-
|
|
134
|
-
} finally {
|
|
135
|
-
// Cleanup
|
|
136
|
-
console.log('๐งน Cleaning up...');
|
|
137
|
-
cache.destroy();
|
|
138
|
-
await rm(demoDir, { recursive: true });
|
|
139
|
-
console.log('โ
Demo completed successfully!');
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
if(import.meta.url === `file://${process.argv[1]}`) {
|
|
144
|
-
main().catch(console.error);
|
|
145
|
-
}
|