@lexho111/plainblog 0.7.3 → 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 -87
- package/Article.ts +97 -0
- package/AssetManager.js +63 -116
- package/Blog.js +13 -6
- package/cluster-server.js +36 -31
- package/index.js +2 -0
- package/model/SqliteAdapter.js +1 -1
- package/modules/csscompiler/compile-style-worker.js +72 -0
- package/modules/csscompiler/csscompiler.js +135 -0
- package/{workers/compiler-worker.js → modules/jscompiler/compile-js-worker.js} +4 -9
- package/{build-scripts.js → modules/jscompiler/jscompiler.js} +3 -1
- package/package.json +4 -7
- package/public/styles.min.css +8 -2
- 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/Article.js
CHANGED
|
@@ -1,93 +1,88 @@
|
|
|
1
|
-
export default class Article {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const buttons = loggedin // prettier-ignore
|
|
81
|
-
? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
|
|
82
|
-
: "";
|
|
83
|
-
// prettier-ignore
|
|
84
|
-
return `<article data-id="${this.id}" data-date="${this.createdAt}">
|
|
1
|
+
export default class Article {
|
|
2
|
+
constructor(id, title, content, createdAt) {
|
|
3
|
+
this.id = id;
|
|
4
|
+
this.title = title;
|
|
5
|
+
this.content = content;
|
|
6
|
+
this.createdAt = new Date(createdAt).getTime();
|
|
7
|
+
}
|
|
8
|
+
static createNew(title, content, createdAt = new Date()) {
|
|
9
|
+
const id = this.getNextID(); // Your existing ID generator
|
|
10
|
+
//const createdAt = new Date();
|
|
11
|
+
return new Article(id, title, content, createdAt);
|
|
12
|
+
}
|
|
13
|
+
static getNextID() {
|
|
14
|
+
// Generiert eine kollisionssichere ID basierend auf Zeitstempel + Zufall
|
|
15
|
+
// Passt in JS Safe Integer und SQLite Integer (64-bit)
|
|
16
|
+
return Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
17
|
+
}
|
|
18
|
+
getContent() {
|
|
19
|
+
return this.content;
|
|
20
|
+
}
|
|
21
|
+
getContentVeryShort() {
|
|
22
|
+
if (!this.content)
|
|
23
|
+
return "";
|
|
24
|
+
if (this.content.length < 50)
|
|
25
|
+
return this.content;
|
|
26
|
+
let contentShort = this.content.substring(0, 50);
|
|
27
|
+
contentShort += "...";
|
|
28
|
+
return contentShort;
|
|
29
|
+
}
|
|
30
|
+
getContentShort() {
|
|
31
|
+
if (!this.content)
|
|
32
|
+
return "";
|
|
33
|
+
if (this.content.length < 400)
|
|
34
|
+
return this.content;
|
|
35
|
+
let contentShort = this.content.substring(0, 400);
|
|
36
|
+
contentShort += "...";
|
|
37
|
+
return contentShort;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns a JSON-serializable representation of the article.
|
|
41
|
+
* This method is automatically called by JSON.stringify().
|
|
42
|
+
*/
|
|
43
|
+
toJSON() {
|
|
44
|
+
// Return a plain object with the desired properties
|
|
45
|
+
const date = !isNaN(this.createdAt)
|
|
46
|
+
? new Date(this.createdAt).toISOString()
|
|
47
|
+
: new Date().toISOString();
|
|
48
|
+
return {
|
|
49
|
+
id: this.id,
|
|
50
|
+
title: this.title,
|
|
51
|
+
content: this.content,
|
|
52
|
+
createdAt: date,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns a JSON-serializable representation of the article.
|
|
57
|
+
* This method is automatically called by JSON.stringify().
|
|
58
|
+
*/
|
|
59
|
+
json() {
|
|
60
|
+
return this.toJSON();
|
|
61
|
+
}
|
|
62
|
+
getFormattedDate() {
|
|
63
|
+
const date = new Date(this.createdAt);
|
|
64
|
+
const year = date.getFullYear();
|
|
65
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
66
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
67
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
68
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
69
|
+
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
70
|
+
}
|
|
71
|
+
toHTML(loggedin = false) {
|
|
72
|
+
if (this.content == null)
|
|
73
|
+
return "";
|
|
74
|
+
const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
|
|
75
|
+
const buttons = loggedin // prettier-ignore
|
|
76
|
+
? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
|
|
77
|
+
: "";
|
|
78
|
+
// prettier-ignore
|
|
79
|
+
return `<article data-id="${this.id}" data-date="${this.createdAt}">
|
|
85
80
|
${buttons}
|
|
86
81
|
<h2>${this.title}</h2>
|
|
87
82
|
<span class="datetime">${this.getFormattedDate()}</span>
|
|
88
83
|
<p>${this.getContentShort()}</p>
|
|
89
84
|
<div class="full-content" style="display: none;">${this.content}</div>
|
|
90
85
|
${moreButton}
|
|
91
|
-
</article>`;
|
|
92
|
-
|
|
93
|
-
}
|
|
86
|
+
</article>`;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/Article.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export default class Article {
|
|
2
|
+
id: number;
|
|
3
|
+
title: string;
|
|
4
|
+
content: string;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
|
|
7
|
+
constructor(id: number, title: string, content: string, createdAt: string | number | Date) {
|
|
8
|
+
this.id = id;
|
|
9
|
+
this.title = title;
|
|
10
|
+
this.content = content;
|
|
11
|
+
this.createdAt = new Date(createdAt).getTime();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static createNew(title: string, content: string, createdAt: string | number | Date = new Date()): Article {
|
|
15
|
+
const id = this.getNextID(); // Your existing ID generator
|
|
16
|
+
//const createdAt = new Date();
|
|
17
|
+
return new Article(id, title, content, createdAt);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static getNextID(): number {
|
|
21
|
+
// Generiert eine kollisionssichere ID basierend auf Zeitstempel + Zufall
|
|
22
|
+
// Passt in JS Safe Integer und SQLite Integer (64-bit)
|
|
23
|
+
return Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getContent(): string {
|
|
27
|
+
return this.content;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getContentVeryShort(): string {
|
|
31
|
+
if (!this.content) return "";
|
|
32
|
+
if (this.content.length < 50) return this.content;
|
|
33
|
+
let contentShort = this.content.substring(0, 50);
|
|
34
|
+
contentShort += "...";
|
|
35
|
+
return contentShort;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getContentShort(): string {
|
|
39
|
+
if (!this.content) return "";
|
|
40
|
+
if (this.content.length < 400) return this.content;
|
|
41
|
+
let contentShort = this.content.substring(0, 400);
|
|
42
|
+
contentShort += "...";
|
|
43
|
+
return contentShort;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns a JSON-serializable representation of the article.
|
|
48
|
+
* This method is automatically called by JSON.stringify().
|
|
49
|
+
*/
|
|
50
|
+
toJSON(): { id: number; title: string; content: string; createdAt: string } {
|
|
51
|
+
// Return a plain object with the desired properties
|
|
52
|
+
const date = !isNaN(this.createdAt)
|
|
53
|
+
? new Date(this.createdAt).toISOString()
|
|
54
|
+
: new Date().toISOString();
|
|
55
|
+
return {
|
|
56
|
+
id: this.id,
|
|
57
|
+
title: this.title,
|
|
58
|
+
content: this.content,
|
|
59
|
+
createdAt: date,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns a JSON-serializable representation of the article.
|
|
65
|
+
* This method is automatically called by JSON.stringify().
|
|
66
|
+
*/
|
|
67
|
+
json(): { id: number; title: string; content: string; createdAt: string } {
|
|
68
|
+
return this.toJSON();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getFormattedDate(): string {
|
|
72
|
+
const date = new Date(this.createdAt);
|
|
73
|
+
const year = date.getFullYear();
|
|
74
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
75
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
76
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
77
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
78
|
+
return `${year}/${month}/${day} ${hours}:${minutes}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
toHTML(loggedin = false): string {
|
|
82
|
+
if (this.content == null) return "";
|
|
83
|
+
const moreButton = this.content.length > 400 ? `<a href="#">more</a>` : "";
|
|
84
|
+
const buttons = loggedin // prettier-ignore
|
|
85
|
+
? `<div class="buttons"><div id="editButton${this.id}" class="btn edit" role="button" tabindex="0" data-action="edit" data-id="${this.id}">edit</div><div id="deleteButton${this.id}" class="btn delete" role="button" tabindex="0" data-action="delete" data-id="${this.id}">delete</div><div id="somethingelseButton${this.id}" class="btn light">something else</div></div>`
|
|
86
|
+
: "";
|
|
87
|
+
// prettier-ignore
|
|
88
|
+
return `<article data-id="${this.id}" data-date="${this.createdAt}">
|
|
89
|
+
${buttons}
|
|
90
|
+
<h2>${this.title}</h2>
|
|
91
|
+
<span class="datetime">${this.getFormattedDate()}</span>
|
|
92
|
+
<p>${this.getContentShort()}</p>
|
|
93
|
+
<div class="full-content" style="display: none;">${this.content}</div>
|
|
94
|
+
${moreButton}
|
|
95
|
+
</article>`;
|
|
96
|
+
}
|
|
97
|
+
}
|
package/AssetManager.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { Worker } from "node:worker_threads";
|
|
6
6
|
import { createDebug } from "./debug-loader.js";
|
|
7
|
+
import { checkSourceCSS } from "./modules/csscompiler/csscompiler.js";
|
|
7
8
|
|
|
8
9
|
const debug = createDebug("plainblog:AssetManager");
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -13,27 +14,30 @@ export default class AssetManager {
|
|
|
13
14
|
constructor() {
|
|
14
15
|
this.publicDir = path.join(process.cwd(), "public");
|
|
15
16
|
this.reloadStylesOnGET = false;
|
|
16
|
-
this.
|
|
17
|
+
this._stylesheetPath = null;
|
|
17
18
|
this.compilestyle = false;
|
|
18
|
-
this.
|
|
19
|
-
this.compiledStyles = "";
|
|
20
|
-
this.stylesHash = "";
|
|
19
|
+
this._styles = "";
|
|
21
20
|
this.scriptsHash = "";
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
set style(value) {
|
|
24
|
+
debug("set style %s", value);
|
|
25
|
+
this._styles += value;
|
|
26
26
|
this.compilestyle = true;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
set stylesheetPath(value) {
|
|
30
|
+
debug(`set Stylesheet path: ${value}`);
|
|
31
|
+
this._stylesheetPath = value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get stylesheetPath() {
|
|
35
|
+
return this._stylesheetPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
async init() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await this.processStylesheets(this.stylesheetPath);
|
|
33
|
-
}
|
|
34
|
-
if (!this.stylesheetPath) {
|
|
35
|
-
await this.processDefaultStyles();
|
|
36
|
-
}
|
|
39
|
+
debug("init");
|
|
40
|
+
await this.processStyles();
|
|
37
41
|
|
|
38
42
|
// Process Scripts
|
|
39
43
|
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
@@ -42,112 +46,35 @@ export default class AssetManager {
|
|
|
42
46
|
|
|
43
47
|
async reload() {
|
|
44
48
|
debug("reload");
|
|
45
|
-
|
|
46
|
-
await this.processStylesheets(this.stylesheetPath);
|
|
47
|
-
}
|
|
48
|
-
if (!this.stylesheetPath) {
|
|
49
|
-
await this.processDefaultStyles();
|
|
50
|
-
}
|
|
49
|
+
await this.processStyles();
|
|
51
50
|
const srcScriptPath = path.join(__dirname, "src", "fetchData.js");
|
|
52
51
|
await this.processScripts(srcScriptPath);
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
async
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
let srcStyles = "";
|
|
62
|
-
|
|
63
|
-
await Promise.all([
|
|
64
|
-
fs.promises
|
|
65
|
-
.readFile(publicStylePath, "utf8")
|
|
66
|
-
.then((publicCSS) => {
|
|
67
|
-
const match = publicCSS.match(
|
|
68
|
-
/\/\* source-hash: ([a-f0-9]{64}) \*\//,
|
|
69
|
-
);
|
|
70
|
-
if (match) publicHash = match[1];
|
|
71
|
-
})
|
|
72
|
-
.catch((err) => {
|
|
73
|
-
if (err.code !== "ENOENT" && !err.message.includes("ENOENT"))
|
|
74
|
-
console.error(err);
|
|
75
|
-
}),
|
|
76
|
-
fs.promises
|
|
77
|
-
.readFile(srcStylePath, "utf8")
|
|
78
|
-
.then((content) => {
|
|
79
|
-
srcStyles = content;
|
|
80
|
-
})
|
|
81
|
-
.catch((err) => {
|
|
82
|
-
if (err.code !== "ENOENT" && !err.message.includes("ENOENT"))
|
|
83
|
-
console.error(err);
|
|
84
|
-
}),
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const combinedStyles = srcStyles + "\n" + this.styles;
|
|
88
|
-
const srcHash = crypto
|
|
89
|
-
.createHash("sha256")
|
|
90
|
-
.update(combinedStyles)
|
|
91
|
-
.digest("hex");
|
|
92
|
-
|
|
93
|
-
if (srcHash !== publicHash) {
|
|
94
|
-
debug("Styles have changed. Recompiling in worker...");
|
|
95
|
-
const finalStyles = await this.runWorker("mergeStyles", [
|
|
96
|
-
srcStyles,
|
|
97
|
-
this.styles,
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
await fs.promises.mkdir(path.dirname(publicStylePath), {
|
|
102
|
-
recursive: true,
|
|
103
|
-
});
|
|
104
|
-
await fs.promises.writeFile(
|
|
105
|
-
publicStylePath,
|
|
106
|
-
finalStyles + `\n/* source-hash: ${srcHash} */`,
|
|
54
|
+
async processStyles(compilerOptions = {}) {
|
|
55
|
+
let resolvedStylesheetPath = this.stylesheetPath;
|
|
56
|
+
if (resolvedStylesheetPath) {
|
|
57
|
+
if (Array.isArray(resolvedStylesheetPath)) {
|
|
58
|
+
resolvedStylesheetPath = resolvedStylesheetPath.map((p) =>
|
|
59
|
+
path.isAbsolute(p) ? p : path.resolve(process.cwd(), p),
|
|
107
60
|
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
debug("process stylesheets");
|
|
116
|
-
const fileList = Array.isArray(files) ? files : [files];
|
|
117
|
-
const styleFiles = fileList.filter(
|
|
118
|
-
(f) =>
|
|
119
|
-
typeof f === "string" &&
|
|
120
|
-
(f.endsWith(".scss") || f.endsWith(".css")) &&
|
|
121
|
-
!f.endsWith(".min.css"),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (styleFiles.length > 0) {
|
|
125
|
-
const fileData = await Promise.all(
|
|
126
|
-
styleFiles.sort().map(async (f) => {
|
|
127
|
-
const content = await fs.promises.readFile(f, "utf-8");
|
|
128
|
-
if (content == "") throw new Error("Invalid Filepath or empty file!");
|
|
129
|
-
return { path: f, content };
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
const hash = crypto.createHash("sha256");
|
|
134
|
-
for (const file of fileData) hash.update(file.content);
|
|
135
|
-
const currentHash = hash.digest("hex");
|
|
136
|
-
|
|
137
|
-
if (currentHash !== this.stylesHash && this.compilestyle) {
|
|
138
|
-
console.log("style assets have changed. recompiling in worker...");
|
|
139
|
-
this.stylesHash = currentHash;
|
|
140
|
-
this.compiledStyles = await this.runWorker("styles", fileData);
|
|
141
|
-
|
|
142
|
-
await fs.promises.mkdir(this.publicDir, { recursive: true });
|
|
143
|
-
await fs.promises.writeFile(
|
|
144
|
-
path.join(this.publicDir, "styles.min.css"),
|
|
145
|
-
this.compiledStyles + `\n/* source-hash: ${currentHash} */`,
|
|
61
|
+
} else if (
|
|
62
|
+
typeof resolvedStylesheetPath === "string" &&
|
|
63
|
+
!path.isAbsolute(resolvedStylesheetPath)
|
|
64
|
+
) {
|
|
65
|
+
resolvedStylesheetPath = path.resolve(
|
|
66
|
+
process.cwd(),
|
|
67
|
+
resolvedStylesheetPath,
|
|
146
68
|
);
|
|
147
|
-
} else {
|
|
148
|
-
console.log("styles are up-to-date");
|
|
149
69
|
}
|
|
150
70
|
}
|
|
71
|
+
await checkSourceCSS({
|
|
72
|
+
stylesheetPath: resolvedStylesheetPath,
|
|
73
|
+
style: this._styles,
|
|
74
|
+
outputDir: this.publicDir,
|
|
75
|
+
outputPath: path.join(this.publicDir, "styles.min.css"),
|
|
76
|
+
...compilerOptions,
|
|
77
|
+
});
|
|
151
78
|
}
|
|
152
79
|
|
|
153
80
|
async processScripts(files) {
|
|
@@ -184,14 +111,28 @@ export default class AssetManager {
|
|
|
184
111
|
}
|
|
185
112
|
|
|
186
113
|
if (currentHash !== this.scriptsHash) {
|
|
187
|
-
console.log("script assets have changed. recompiling
|
|
188
|
-
|
|
189
|
-
|
|
114
|
+
console.log("script assets have changed. recompiling...");
|
|
115
|
+
let compiledScripts;
|
|
116
|
+
let success = false;
|
|
117
|
+
try {
|
|
118
|
+
compiledScripts = await this.runWorker("scripts", fileData);
|
|
119
|
+
success = true;
|
|
120
|
+
this.scriptsHash = currentHash;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn(
|
|
123
|
+
"Script compilation failed (using fallback):",
|
|
124
|
+
e.message,
|
|
125
|
+
);
|
|
126
|
+
compiledScripts = fileData.map((f) => f.content).join(";\n");
|
|
127
|
+
}
|
|
190
128
|
|
|
191
129
|
await fs.promises.mkdir(this.publicDir, { recursive: true });
|
|
130
|
+
const content = success
|
|
131
|
+
? compiledScripts + `\n/* source-hash: ${currentHash} */`
|
|
132
|
+
: compiledScripts;
|
|
192
133
|
await fs.promises.writeFile(
|
|
193
134
|
path.join(this.publicDir, "scripts.min.js"),
|
|
194
|
-
|
|
135
|
+
content,
|
|
195
136
|
);
|
|
196
137
|
}
|
|
197
138
|
}
|
|
@@ -205,11 +146,17 @@ export default class AssetManager {
|
|
|
205
146
|
: { type, fileData: data };
|
|
206
147
|
|
|
207
148
|
const worker = new Worker(
|
|
208
|
-
path.resolve(
|
|
149
|
+
path.resolve(
|
|
150
|
+
__dirname,
|
|
151
|
+
"modules",
|
|
152
|
+
"jscompiler",
|
|
153
|
+
"compile-js-worker.js",
|
|
154
|
+
),
|
|
209
155
|
{ workerData },
|
|
210
156
|
);
|
|
211
157
|
worker.on("message", (msg) => {
|
|
212
158
|
if (msg.status === "success") {
|
|
159
|
+
console.log("compilation complete: public/styles.min.css created");
|
|
213
160
|
resolve(msg.result);
|
|
214
161
|
} else {
|
|
215
162
|
reject(new Error(msg.error));
|
package/Blog.js
CHANGED
|
@@ -123,13 +123,13 @@ export default class Blog {
|
|
|
123
123
|
* @param {string} style - A string containing CSS rules.
|
|
124
124
|
*/
|
|
125
125
|
set style(style) {
|
|
126
|
-
debug("set style");
|
|
127
|
-
this.assetManager.
|
|
126
|
+
debug("set style %s", style);
|
|
127
|
+
this.assetManager.style = style;
|
|
128
128
|
this.assetManager.compilestyle = true;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
get style() {
|
|
132
|
-
return this.assetManager.
|
|
132
|
+
return this.assetManager._styles;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
@@ -137,8 +137,8 @@ export default class Blog {
|
|
|
137
137
|
* @param {string|string[]} files - A single file path or an array of file paths.
|
|
138
138
|
*/
|
|
139
139
|
set stylesheetPath(files) {
|
|
140
|
+
debug(`set stylesheet path: ${files}`);
|
|
140
141
|
this.assetManager.stylesheetPath = files;
|
|
141
|
-
console.log(`this.assetManager.stylesheetPath: ${files}`);
|
|
142
142
|
this.assetManager.compilestyle = true;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -160,11 +160,18 @@ export default class Blog {
|
|
|
160
160
|
|
|
161
161
|
/** initializes database */
|
|
162
162
|
async init() {
|
|
163
|
-
if (this.#initPromise) return this.#initPromise;
|
|
163
|
+
//if (this.#initPromise) return this.#initPromise;
|
|
164
164
|
|
|
165
165
|
this.#initPromise = (async () => {
|
|
166
166
|
try {
|
|
167
|
-
|
|
167
|
+
try {
|
|
168
|
+
await this.assetManager.init();
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.warn(
|
|
171
|
+
"Warning: AssetManager initialization failed. Styles may not be compiled.",
|
|
172
|
+
e.message,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
168
175
|
|
|
169
176
|
if (this.#isExternalAPI) {
|
|
170
177
|
console.log("external API");
|
package/cluster-server.js
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
1
1
|
import cluster from "cluster";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
4
5
|
import Blog from "./Blog.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
export function startCluster(port = process.env.PORT || 8080) {
|
|
8
|
+
const numCPUs = os.cpus().length;
|
|
7
9
|
|
|
8
|
-
if (cluster.isPrimary) {
|
|
9
|
-
|
|
10
|
+
if (cluster.isPrimary) {
|
|
11
|
+
console.log(`master ${process.pid} is running`);
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Fork workers (one per CPU core)
|
|
14
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
15
|
+
cluster.fork();
|
|
16
|
+
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} else {
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
cluster.on("exit", (worker, code, signal) => {
|
|
19
|
+
console.log(
|
|
20
|
+
`worker ${worker.process.pid} died (${signal || code}). restarting...`,
|
|
21
|
+
);
|
|
22
|
+
cluster.fork(); // Restart dead worker
|
|
23
|
+
});
|
|
24
|
+
} else {
|
|
25
|
+
// Worker process
|
|
26
|
+
const blog = new Blog();
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
blog
|
|
29
|
+
.startServer(port)
|
|
30
|
+
.then(() => {
|
|
31
|
+
console.log(`worker ${process.pid} started on port ${port}`);
|
|
32
|
+
})
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
console.error(`Worker ${process.pid} failed to start:`, err);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.catch((err) => {
|
|
34
|
-
console.error(`Worker ${process.pid} failed to start:`, err);
|
|
35
|
-
process.exit(1);
|
|
38
|
+
// Graceful shutdown
|
|
39
|
+
process.on("SIGTERM", async () => {
|
|
40
|
+
console.log(`worker ${process.pid} shutting down...`);
|
|
41
|
+
await blog.closeServer();
|
|
42
|
+
process.exit(0);
|
|
36
43
|
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log(`worker ${process.pid} shutting down...`);
|
|
41
|
-
await blog.closeServer();
|
|
42
|
-
process.exit(0);
|
|
43
|
-
});
|
|
47
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
48
|
+
startCluster();
|
|
44
49
|
}
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import FileAdapter from "./model/FileAdapter.js";
|
|
|
4
4
|
import PostgresAdapter from "./model/PostgresAdapter.js";
|
|
5
5
|
import SequelizeAdapter from "./model/SequelizeAdapter.js";
|
|
6
6
|
import SqliteAdapter from "./model/SqliteAdapter.js";
|
|
7
|
+
import { startCluster } from "./cluster-server.js";
|
|
7
8
|
|
|
8
9
|
export {
|
|
9
10
|
Blog,
|
|
@@ -12,5 +13,6 @@ export {
|
|
|
12
13
|
SequelizeAdapter,
|
|
13
14
|
FileAdapter,
|
|
14
15
|
SqliteAdapter,
|
|
16
|
+
startCluster,
|
|
15
17
|
};
|
|
16
18
|
export default Blog;
|
package/model/SqliteAdapter.js
CHANGED
|
@@ -32,7 +32,7 @@ export default class SqliteAdapter {
|
|
|
32
32
|
console.error(
|
|
33
33
|
"The 'better-sqlite3' package is not installed. Please install it by running: npm install better-sqlite3",
|
|
34
34
|
);
|
|
35
|
-
|
|
35
|
+
process.exit(1);
|
|
36
36
|
} else {
|
|
37
37
|
throw error;
|
|
38
38
|
}
|