@lexho111/plainblog 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Article.js +82 -90
- package/Article.ts +97 -0
- package/AssetManager.js +63 -116
- package/Auth.js +2 -2
- package/Blog.js +103 -82
- package/cluster-server.js +36 -31
- package/debug-loader.js +16 -1
- package/index.js +2 -0
- package/model/DataModel.js +11 -9
- package/model/SqliteAdapter.js +15 -7
- package/model/datastructures/BinarySearchTree.js +1 -1
- package/modules/csscompiler/compile-style-worker.js +72 -0
- package/modules/csscompiler/csscompiler.js +135 -0
- package/modules/jscompiler/compile-js-worker.js +34 -0
- package/{build-scripts.js → modules/jscompiler/jscompiler.js} +3 -1
- package/package.json +4 -7
- package/public/styles.min.css +8 -2
- package/router.js +60 -64
- package/server.js +190 -155
- package/src/styles.css +1 -1
- package/styles.hash +1 -0
- package/tsconfig.json +12 -0
- package/.dependency-cruiser.cjs +0 -382
- package/ArrayList.cpuprofile +0 -1
- package/BinarySearchTree.cpuprofile +0 -1
- package/FlameChartProfile.cpuprofile +0 -1
- package/FlameChartProfile2.cpuprofile +0 -1
- package/FlameChartProfile3.cpuprofile +0 -1
- package/StandaloneProfile.cpuprofile +0 -1
- package/blog_test_empty +0 -0
- package/blog_test_load +0 -0
- package/build-styles.js +0 -87
- package/workers/compiler-worker.js +0 -42
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { Worker } from "worker_threads";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
// Resolve package root from modules/csscompiler/
|
|
10
|
+
const packageRoot = path.resolve(__dirname, "../../");
|
|
11
|
+
|
|
12
|
+
// css sources
|
|
13
|
+
// in cascadining order
|
|
14
|
+
// 1. src/styles.css // the base
|
|
15
|
+
const defaultBaseStylePath = path.join(packageRoot, "src", "styles.css");
|
|
16
|
+
|
|
17
|
+
const defaultOutputDir = path.join(packageRoot, "public");
|
|
18
|
+
const defaultOutputPath = path.join(defaultOutputDir, "styles.min.css");
|
|
19
|
+
const defaultHashPath = path.join(packageRoot, "styles.hash");
|
|
20
|
+
|
|
21
|
+
function getFileContent(filePath) {
|
|
22
|
+
if (!filePath) return "";
|
|
23
|
+
if (Array.isArray(filePath)) {
|
|
24
|
+
return filePath.map((f) => getFileContent(f)).join("\n");
|
|
25
|
+
}
|
|
26
|
+
const fullPath = path.isAbsolute(filePath)
|
|
27
|
+
? filePath
|
|
28
|
+
: path.join(packageRoot, filePath);
|
|
29
|
+
if (fs.existsSync(fullPath)) {
|
|
30
|
+
return fs.readFileSync(fullPath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
console.warn(`[csscompiler] Warning: Stylesheet not found: ${fullPath}`);
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function computeHash(content) {
|
|
37
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const locks = new Map();
|
|
41
|
+
|
|
42
|
+
export function checkSourceCSS(options = {}) {
|
|
43
|
+
const outputPath = options.outputPath || defaultOutputPath;
|
|
44
|
+
|
|
45
|
+
if (!locks.has(outputPath)) {
|
|
46
|
+
locks.set(outputPath, Promise.resolve());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const nextPromise = locks.get(outputPath).then(
|
|
50
|
+
() => doCheckSourceCSS(options),
|
|
51
|
+
() => doCheckSourceCSS(options),
|
|
52
|
+
);
|
|
53
|
+
locks.set(outputPath, nextPromise);
|
|
54
|
+
|
|
55
|
+
return nextPromise;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function doCheckSourceCSS({
|
|
59
|
+
baseStylePath = defaultBaseStylePath,
|
|
60
|
+
stylesheetPath = null,
|
|
61
|
+
style = "",
|
|
62
|
+
outputDir = defaultOutputDir,
|
|
63
|
+
outputPath = defaultOutputPath,
|
|
64
|
+
hashPath = defaultHashPath,
|
|
65
|
+
...options
|
|
66
|
+
} = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
// something changed?
|
|
69
|
+
const baseContent = getFileContent(baseStylePath);
|
|
70
|
+
const extraContent = getFileContent(stylesheetPath);
|
|
71
|
+
const inlineContent = options.inlineContent || style || "";
|
|
72
|
+
|
|
73
|
+
const combinedContent = JSON.stringify({
|
|
74
|
+
baseContent,
|
|
75
|
+
extraContent,
|
|
76
|
+
inlineContent,
|
|
77
|
+
});
|
|
78
|
+
const currentHash = computeHash(combinedContent);
|
|
79
|
+
|
|
80
|
+
let previousHash = "";
|
|
81
|
+
if (fs.existsSync(hashPath)) {
|
|
82
|
+
previousHash = fs.readFileSync(hashPath, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (currentHash === previousHash && fs.existsSync(outputPath)) {
|
|
86
|
+
// no --> keep old "public/styles.min.css"
|
|
87
|
+
//console.log("No changes detected.");
|
|
88
|
+
resolve(false);
|
|
89
|
+
} else {
|
|
90
|
+
// yes -> something changed
|
|
91
|
+
// run worker 'compile"
|
|
92
|
+
console.log("style assets have changed. recompiling...");
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(outputDir)) {
|
|
95
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const worker = new Worker(
|
|
99
|
+
path.join(__dirname, "compile-style-worker.js"),
|
|
100
|
+
{
|
|
101
|
+
workerData: {
|
|
102
|
+
baseContent,
|
|
103
|
+
extraContent,
|
|
104
|
+
inlineContent,
|
|
105
|
+
outputPath,
|
|
106
|
+
...options,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
worker.on("message", (msg) => {
|
|
112
|
+
if (msg === "success") {
|
|
113
|
+
fs.writeFileSync(hashPath, currentHash);
|
|
114
|
+
console.log("compilation complete: public/styles.min.css created");
|
|
115
|
+
resolve(true);
|
|
116
|
+
} else if (msg && msg.error) {
|
|
117
|
+
console.error("Worker compilation failed:", msg.error);
|
|
118
|
+
reject(new Error(msg.error));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
worker.on("error", (err) => {
|
|
123
|
+
console.error("Worker error:", err);
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
worker.on("exit", (code) => {
|
|
127
|
+
if (code !== 0) {
|
|
128
|
+
const error = new Error(`Worker stopped with exit code ${code}`);
|
|
129
|
+
console.error(error);
|
|
130
|
+
reject(error);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parentPort, workerData } from "worker_threads";
|
|
2
|
+
import { compileScripts } from "./jscompiler.js";
|
|
3
|
+
import { measure_perf } from "../../debug-loader.js";
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
if (!parentPort) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const { type, fileData } = workerData;
|
|
10
|
+
try {
|
|
11
|
+
const result = await measure_perf("compilerWorker", async () => {
|
|
12
|
+
if (type === "scripts") {
|
|
13
|
+
return await compileScripts(fileData);
|
|
14
|
+
} else {
|
|
15
|
+
throw new Error(`Unknown compilation type: ${type}`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
parentPort.postMessage({ status: "success", result });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
parentPort.postMessage({ status: "error", error: error.message });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
run();
|
|
25
|
+
|
|
26
|
+
process.on("uncaughtException", (err) => {
|
|
27
|
+
console.error("KRITISCHER FEHLER:", err.stack);
|
|
28
|
+
// Logge den Fehler in eine Datei oder einen Dienst
|
|
29
|
+
process.exit(1); // Kontrollierter Neustart durch PM2
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
33
|
+
console.error("UNBEHANDELTES PROMISE:", reason);
|
|
34
|
+
});
|
|
@@ -34,7 +34,9 @@ async function runBabelAndUglify(js) {
|
|
|
34
34
|
presetEnv = presetModule.default || presetModule;
|
|
35
35
|
} catch (error) {
|
|
36
36
|
console.error("Build scripts failed. Missing dependencies.");
|
|
37
|
-
console.error(
|
|
37
|
+
console.error(
|
|
38
|
+
"Please run: npm install @babel/core uglify-js @babel/preset-env",
|
|
39
|
+
);
|
|
38
40
|
throw new Error("Missing optional dependencies");
|
|
39
41
|
}
|
|
40
42
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexho111/plainblog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "A tool for creating and serving a minimalist, single-page blog.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test2": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
13
13
|
"coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage & start coverage/lcov-report/index.html",
|
|
14
14
|
"view-coverage": "start coverage/lcov-report/index.html",
|
|
15
|
-
"lint": "eslint ."
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"tsc": "npx tsc"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"blog",
|
|
@@ -26,8 +27,6 @@
|
|
|
26
27
|
"@babel/preset-env": "^7.28.6",
|
|
27
28
|
"@eslint/js": "^9.39.2",
|
|
28
29
|
"@types/node": "^25.0.3",
|
|
29
|
-
"autoprefixer": "^10.4.23",
|
|
30
|
-
"cssnano": "^7.1.2",
|
|
31
30
|
"debug": "^4.4.3",
|
|
32
31
|
"dependency-cruiser": "^17.3.7",
|
|
33
32
|
"eslint": "^9.39.2",
|
|
@@ -37,7 +36,6 @@
|
|
|
37
36
|
"jest": "^29.7.0",
|
|
38
37
|
"pg": "^8.16.3",
|
|
39
38
|
"pg-hstore": "^2.3.4",
|
|
40
|
-
"postcss": "^8.5.6",
|
|
41
39
|
"sequelize": "^6.37.7",
|
|
42
40
|
"sqlite3": "^5.0.2",
|
|
43
41
|
"typescript": "^5.9.3",
|
|
@@ -46,7 +44,6 @@
|
|
|
46
44
|
},
|
|
47
45
|
"dependencies": {
|
|
48
46
|
"async-lock": "^1.4.1",
|
|
49
|
-
"better-sqlite3": "^12.6.2"
|
|
50
|
-
"child_process": "^1.0.2"
|
|
47
|
+
"better-sqlite3": "^12.6.2"
|
|
51
48
|
}
|
|
52
49
|
}
|
package/public/styles.min.css
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
:root{--black:#111;--clearwhite:#fefefe;--white:#
|
|
2
|
-
|
|
1
|
+
:root{--black:#111;--clearwhite:#fefefe;--white:#eeeeee;--darkgray:rgba(54, 54, 54, 0.5);--text-primary:var(--black);--text-secondary:var(--clearwhite);--max-width:600px}html,
|
|
2
|
+
body{margin:0;overflow-x:hidden}html{font-size:clamp(0.85rem, 1vw + 0.5rem, 1.1rem)}body{color:var(--text-primary);background:var(--white_darker);font-family:Arial}nav{margin-top:10px;display:flex;align-items:center;max-width:var(--max-width);background:hsl(0deg 0% 92.27%);padding:5px;box-sizing:border-box}nav,
|
|
3
|
+
#header h1{}#header{max-width:var(--max-width)}form input,
|
|
4
|
+
form textarea,
|
|
5
|
+
button{padding:5px 10px;box-sizing:border-box}button{width:fit-content;padding:10px}img{max-width:100%}input,
|
|
6
|
+
textarea{box-shadow:6px 6px 1px 1px rgb(50 50 50 / 0.2);border:1px solid #ababab}input:focus,
|
|
7
|
+
textarea:focus{outline:none;border:2px solid #3b40c1;box-shadow:6px 6px 1px 1px rgb(50 50 50 / 0.2);background-color:var(--clearwhite)}.form_element{display:block;margin-bottom:10px;padding:4px;font-family:monospace;font-size:103%}.wide{width:100%}.password{width:250px}.new_title{padding:10px;height:35px}.new_content{height:300px;resize:none;padding:10px}#createNew{width:100%;max-width:var(--max-width)}#search{margin-left:auto;font-size:1rem;padding:0.4rem 1rem;border:1px solid var(--text-primary);box-shadow:none}hr{max-width:var(--max-width);margin-left:0;margin:40px 0}.articles{border:0 solid #000;display:grid;gap:0.25rem;grid-template-columns:1fr;max-width:var(--max-width)}.articles article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:0.4rem;margin-bottom:10px;border:2px solid darkgray}.articles article h2{color:rgb(53, 53, 53);margin-bottom:5px}.articles article .datetime{margin:0;color:rgb(117, 117, 117)}.articles article p{margin-top:10px;margin-bottom:0}article a{color:rgb(105, 105, 105)}article a:visited{color:rgb(105, 105, 105)}h1{color:#696969}h2{margin-top:0;border:0px solid black}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}.loginform{margin-left:25px}#wrapper{max-width:1200px;padding:0 20px;width:100%;margin-left:0;box-sizing:border-box}.buttons{box-sizing:border-box;display:flex;align-items:center;gap:5px;list-style:none;margin:0;margin-bottom:16px;padding:15px;padding-left:0px;border:0px solid black;width:100%;height:25px}.box{}.btn{border:none;color:var(--text-primary);font-size:1rem;padding:0.4rem 1rem;font-weight:500;text-decoration:none;cursor:pointer;border-radius:0px;width:fit-content;border:1px solid var(--text-primary);box-shadow:3px 2px 2px var(--darkgray)}.btn:hover{color:black;background-color:white;border:2px solid var(--black)}.light{color:var(--black);background:var(--clearwhite)}.light:hover{color:var(--clearwhite);background:var(--black)}.login{font-weight:600}.hide-image{display:none}.edit{color:var(--clearwhite);background-color:blue}.delete{color:var(--clearwhite);background-color:red}@media screen and (min-width:1000px){#wrapper,
|
|
8
|
+
body{width:90%;max-width:1400px;padding:0 40px;margin:0 auto;font-size:0.75rem}.form_element{font-size:105%}.articles article p{font-size:105%}}
|
package/router.js
CHANGED
|
@@ -9,112 +9,108 @@ const debug = createDebug("plainblog:api");
|
|
|
9
9
|
const staticCache = new Map();
|
|
10
10
|
|
|
11
11
|
// API routes
|
|
12
|
-
export
|
|
12
|
+
export function login(req, res, cb, cb2) {
|
|
13
13
|
// workaround for angular frontend
|
|
14
14
|
if (
|
|
15
15
|
(req.url === "/api/login" || req.url === "/login") &&
|
|
16
16
|
req.method === "POST"
|
|
17
17
|
) {
|
|
18
18
|
debug(`${req.method} ${req.url}`);
|
|
19
|
-
|
|
20
|
-
return;
|
|
19
|
+
return cb(req, res);
|
|
21
20
|
}
|
|
22
21
|
if (req.method === "GET" && req.url === "/login") {
|
|
23
22
|
debug(`${req.method} ${req.url}`);
|
|
24
23
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
25
|
-
|
|
26
|
-
return;
|
|
24
|
+
return cb2(req, res);
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
export
|
|
28
|
+
export function logout(req, res, cb) {
|
|
31
29
|
if (req.method === "POST") {
|
|
32
30
|
if (req.url === "/logout" || req.url === "/api/logout") {
|
|
33
31
|
debug(`${req.method} ${req.url}`);
|
|
34
|
-
|
|
35
|
-
return;
|
|
32
|
+
return cb(req, res);
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
export
|
|
37
|
+
export function api(req, res, cb) {
|
|
41
38
|
if (req.url.startsWith("/api")) {
|
|
42
39
|
debug(`${req.method} ${req.url}`);
|
|
43
|
-
|
|
44
|
-
return;
|
|
40
|
+
return cb(req, res);
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
export
|
|
44
|
+
export function new1(req, res, cb) {
|
|
49
45
|
if (req.method === "POST" && req.url === "/new") {
|
|
50
46
|
const time_start = performance.now();
|
|
51
47
|
debug(`${req.method} ${req.url}`);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
return cb(req, res).then(() => {
|
|
49
|
+
const time_end = performance.now();
|
|
50
|
+
const duration = time_end - time_start;
|
|
51
|
+
debug_perf('"/new"', duration);
|
|
52
|
+
});
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
export
|
|
56
|
+
export function handleLogin(req, res, cb) {
|
|
61
57
|
if (req.method === "POST") {
|
|
62
58
|
debug(`${req.method} ${req.url}`);
|
|
63
|
-
|
|
64
|
-
return;
|
|
59
|
+
return cb(req, res);
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
export
|
|
63
|
+
export function getPages(req, res, cb, publicDir) {
|
|
69
64
|
if (req.method === "GET") {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Try to serve static files from public folder
|
|
80
|
-
// Normalize path to prevent directory traversal attacks
|
|
81
|
-
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
82
|
-
const filePath = path.join(
|
|
83
|
-
publicDir,
|
|
84
|
-
safePath === "/" ? "index.html" : safePath,
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
//debug("%s", filePath);
|
|
88
|
-
try {
|
|
89
|
-
let data;
|
|
90
|
-
if (staticCache.has(filePath)) {
|
|
91
|
-
data = staticCache.get(filePath);
|
|
92
|
-
} else {
|
|
93
|
-
data = await readFile(filePath);
|
|
94
|
-
staticCache.set(filePath, data);
|
|
65
|
+
return (async () => {
|
|
66
|
+
const getRoot_start = performance.now();
|
|
67
|
+
if (req.url === "/") {
|
|
68
|
+
debug(`${req.method} ${req.url}`);
|
|
69
|
+
await cb(req, res);
|
|
70
|
+
const getRoot_end = performance.now();
|
|
71
|
+
const duration = getRoot_end - getRoot_start;
|
|
72
|
+
debug_perf('GET "/"', duration);
|
|
73
|
+
return;
|
|
95
74
|
}
|
|
75
|
+
// Try to serve static files from public folder
|
|
76
|
+
// Normalize path to prevent directory traversal attacks
|
|
77
|
+
const safePath = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, "");
|
|
78
|
+
const filePath = path.join(
|
|
79
|
+
publicDir,
|
|
80
|
+
safePath === "/" ? "index.html" : safePath,
|
|
81
|
+
);
|
|
96
82
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
//debug("%s", filePath);
|
|
84
|
+
try {
|
|
85
|
+
let data;
|
|
86
|
+
if (staticCache.has(filePath)) {
|
|
87
|
+
data = staticCache.get(filePath);
|
|
88
|
+
} else {
|
|
89
|
+
data = await readFile(filePath);
|
|
90
|
+
staticCache.set(filePath, data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Manual MIME type detection (simplified)
|
|
94
|
+
const ext = path.extname(filePath);
|
|
95
|
+
const mimeTypes = {
|
|
96
|
+
".html": "text/html",
|
|
97
|
+
".css": "text/css",
|
|
98
|
+
".js": "text/javascript",
|
|
99
|
+
};
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
103
|
+
});
|
|
104
|
+
res.end(data);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err) {
|
|
107
|
+
if (!res.headersSent) {
|
|
108
|
+
res.writeHead(404);
|
|
109
|
+
return res.end("File Not Found");
|
|
110
|
+
}
|
|
114
111
|
}
|
|
115
112
|
}
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
113
|
+
})();
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
// ---------------------------------------------
|