@lexho111/plainblog 0.5.9 → 0.5.10

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/Blog.js CHANGED
@@ -11,6 +11,7 @@ import path from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import { exec } from "child_process";
13
13
  import { promisify } from "util";
14
+ import { compileStyles, mergeStyles } from "./build-styles.js";
14
15
 
15
16
  const execPromise = promisify(exec);
16
17
 
@@ -189,8 +190,7 @@ export default class Blog {
189
190
 
190
191
  if (srcHash !== publicHash) {
191
192
  console.log("Styles have changed. Recompiling...");
192
- //const finalStyles = await mergeStyles(this.styles, srcStyles);
193
- const finalStyles = this.styles + " " + srcStyles;
193
+ const finalStyles = await mergeStyles(this.styles, srcStyles);
194
194
  try {
195
195
  await fs.promises.mkdir(path.dirname(publicStylePath), { recursive: true });
196
196
  await fs.promises.writeFile(publicStylePath, finalStyles + `\n/* source-hash: ${srcHash} */`);
@@ -552,8 +552,7 @@ export default class Blog {
552
552
  this.#stylesHash = currentHash;
553
553
 
554
554
  // Compile styles using the standalone script from build-styles.js
555
- //this.compiledStyles = await compileStyles(fileData);
556
- this.compiledStyles = fileData.map((f) => f.content).join("\n");
555
+ this.compiledStyles = await compileStyles(fileData);
557
556
 
558
557
  // generate a file
559
558
  const __filename = fileURLToPath(import.meta.url);
package/articles.txt CHANGED
@@ -1,3 +1,11 @@
1
1
  {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:19:39.939Z"}
2
2
  {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:34:38.866Z"}
3
3
  {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T13:43:32.343Z"}
4
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:48:23.123Z"}
5
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:50:06.993Z"}
6
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:56:28.369Z"}
7
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:57:53.780Z"}
8
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T18:58:54.261Z"}
9
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:02.613Z"}
10
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:01:30.473Z"}
11
+ {"title":"Test Title from Jest","content":"This is the content of the test article.","createdAt":"2026-01-08T19:03:33.773Z"}
package/blog.db CHANGED
Binary file
package/bloginfo.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "title": "Blog"
2
+ "title": "Test Blog Title"
3
3
  }
@@ -0,0 +1,59 @@
1
+ import path from "path";
2
+ import { pathToFileURL } from "url";
3
+ import postcss from "postcss";
4
+ import autoprefixer from "autoprefixer";
5
+ import cssnano from "cssnano";
6
+
7
+ // array of files or a single file
8
+ export async function compileStyles(fileData) {
9
+ try {
10
+ let combinedCss = "";
11
+
12
+ if (fileData) {
13
+ const scssFiles = fileData.filter(
14
+ (f) =>
15
+ f.path.endsWith(".scss") && !path.basename(f.path).startsWith("_")
16
+ );
17
+
18
+ for (const file of scssFiles) {
19
+ console.error("sass files are not supported.");
20
+ }
21
+
22
+ const cssFiles = fileData.filter((f) => f.path.endsWith(".css"));
23
+ for (const file of cssFiles) {
24
+ combinedCss += file.content + "\n";
25
+ }
26
+ }
27
+
28
+ // 2. PostCSS (Autoprefixer + CSSNano)
29
+ if (combinedCss) {
30
+ const plugins = [autoprefixer(), cssnano()];
31
+ const result = await postcss(plugins).process(combinedCss, {
32
+ from: undefined,
33
+ });
34
+ return result.css;
35
+ }
36
+ return "";
37
+ } catch (error) {
38
+ console.error("Build failed:", error);
39
+ return "";
40
+ }
41
+ }
42
+
43
+ export async function mergeStyles(...cssContents) {
44
+ try {
45
+ const combinedCss = cssContents.join("\n");
46
+
47
+ if (combinedCss) {
48
+ const plugins = [autoprefixer(), cssnano()];
49
+ const result = await postcss(plugins).process(combinedCss, {
50
+ from: undefined,
51
+ });
52
+ return result.css;
53
+ }
54
+ return "";
55
+ } catch (error) {
56
+ console.error("Merge failed:", error);
57
+ return "";
58
+ }
59
+ }
@@ -12,6 +12,7 @@ export default class PostgresAdapter extends SequelizeAdapter {
12
12
  this.host = options.host;
13
13
  if (options.dbname) this.dbname = options.dbname;
14
14
  if (options.dbport) this.dbport = options.dbport;
15
+ else this.dbport = 5432;
15
16
  if (!this.username || !this.password || !this.host) {
16
17
  throw new Error(
17
18
  "PostgreSQL credentials not set. Please provide 'username', 'password', and 'host' in the options."
@@ -21,24 +22,39 @@ export default class PostgresAdapter extends SequelizeAdapter {
21
22
 
22
23
  async initialize() {
23
24
  console.log("initialize database");
25
+ const maxRetries = 10;
26
+ const retryDelay = 3000;
24
27
 
25
- try {
26
- console.log(
27
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
28
- );
28
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
29
+ try {
30
+ console.log(
31
+ `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`
32
+ );
29
33
 
30
- this.sequelize = new Sequelize(
31
- `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
32
- { logging: false }
33
- );
34
- } catch (err) {
35
- if (err.message.includes("Please install")) {
36
- throw new Error(
37
- "PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
34
+ this.sequelize = new Sequelize(
35
+ `postgres://${this.username}:${this.password}@${this.host}:${this.dbport}/${this.dbname}`,
36
+ { logging: false }
37
+ );
38
+ await this.sequelize.authenticate();
39
+ await this.initializeModels();
40
+ console.log("Database connection established successfully.");
41
+ return;
42
+ } catch (err) {
43
+ if (err.message.includes("Please install")) {
44
+ throw new Error(
45
+ "PostgreSQL driver is not installed. Please install it to use PostgresAdapter: npm install pg pg-hstore"
46
+ );
47
+ }
48
+ console.error(
49
+ `Database connection attempt ${attempt} failed: ${err.message}`
38
50
  );
51
+ if (attempt === maxRetries) {
52
+ console.error("Max retries reached. Exiting.");
53
+ throw err;
54
+ }
55
+ console.log(`Retrying in ${retryDelay / 1000} seconds...`);
56
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
39
57
  }
40
- throw err;
41
58
  }
42
- await this.initializeModels();
43
59
  }
44
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexho111/plainblog",
3
- "version": "0.5.9",
3
+ "version": "0.5.10",
4
4
  "description": "A tool for creating and serving a minimalist, single-page blog.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -17,11 +17,15 @@
17
17
  "author": "lexho111",
18
18
  "license": "ISC",
19
19
  "dependencies": {
20
+ "autoprefixer": "^10.4.23",
20
21
  "child_process": "^1.0.2",
22
+ "cssnano": "^7.1.2",
21
23
  "fs": "^0.0.1-security",
22
24
  "http": "^0.0.1-security",
23
25
  "node-fetch": "^3.3.2",
24
26
  "path": "^0.12.7",
27
+ "postcss": "^8.5.6",
28
+ "postcss-preset-env": "^10.6.0",
25
29
  "sass": "^1.97.1",
26
30
  "url": "^0.11.4",
27
31
  "util": "^0.12.5"
@@ -1,68 +1,2 @@
1
- body { font-family: Arial; } .grid {
2
- border: 0 solid #000;
3
- display: grid;
4
- gap: 0.25rem;
5
- grid-template-columns: 1fr;
6
- }
7
- .grid article {
8
- border: 0 solid #ccc;
9
- border-radius: 4px;
10
- min-width: 0;
11
- overflow-wrap: break-word;
12
- padding: 0.25rem;
13
- }
14
- .grid article h2 {
15
- color: rgb(53, 53, 53);
16
- margin-bottom: 5px;
17
- }
18
-
19
- .grid article .datetime {
20
- margin: 0;
21
- color: rgb(117, 117, 117);
22
- }
23
-
24
- .grid article p {
25
- margin-top: 10px;
26
- margin-bottom: 0;
27
- }
28
-
29
- article a {
30
- color: rgb(105, 105, 105);
31
- }
32
-
33
- article a:visited {
34
- color: rgb(105, 105, 105);
35
- }
36
-
37
- h1 {
38
- color: #696969;
39
- }
40
- nav a {
41
- color: #3b40c1;
42
- font-size: 20px;
43
- text-decoration: underline;
44
- }
45
- nav a:visited {
46
- color: #3b40c1;
47
- text-decoration-color: #3b40c1;
48
- }
49
-
50
- #wrapper {
51
- max-width: 500px;
52
- width: 100%;
53
- }
54
-
55
- /* Mobile Layout (screens smaller than 1000px) */
56
- @media screen and (max-width: 1000px) {
57
- * {
58
- font-size: 4vw;
59
- }
60
- #wrapper {
61
- max-width: 100%;
62
- width: 100%;
63
- padding: 0 10px; /* Prevents text from touching the edges */
64
- box-sizing: border-box;
65
- }
66
- }
67
-
68
- /* source-hash: bcb6644ec5b5c6f9685c9ad6c14ee551a6f908b8a2c372d3294e2d2e80d17fb7 */
1
+ body{font-family:Arial;font-family:Arial,sans-serif}h1{color:#333}.grid{border:0 solid #000;display:grid;gap:.25rem;grid-template-columns:1fr}.grid article{border:0 solid #ccc;border-radius:4px;min-width:0;overflow-wrap:break-word;padding:.25rem}.grid article h2{color:#353535;margin-bottom:5px}.grid article .datetime{color:#757575;margin:0}.grid article p{margin-bottom:0;margin-top:10px}article a,article a:visited,h1{color:#696969}nav a{color:#3b40c1;font-size:20px;text-decoration:underline}nav a:visited{color:#3b40c1;text-decoration-color:#3b40c1}#wrapper{max-width:500px;width:100%}@media screen and (max-width:1000px){*{font-size:4vw}#wrapper{box-sizing:border-box;max-width:100%;padding:0 10px;width:100%}}
2
+ /* source-hash: a07f631befba4b6bc703f8709f5ef455faafeff4e5f00b62f835576eea7fb529 */
package/test/blog.test.js CHANGED
@@ -58,13 +58,13 @@ describe("test blog", () => {
58
58
  const styles = [
59
59
  {
60
60
  style: "body { font-family: Courier; }",
61
- expected: "font-family: Courier",
61
+ expected: "font-family:Courier",
62
62
  },
63
63
  {
64
64
  style: "body{ background-color:black; color:white; }",
65
- expected: "background-color:black; color:white",
65
+ expected: "background-color:#000;color:#fff;",
66
66
  },
67
- { style: "body{ font-size: 1.2em; }", expected: "font-size: 1.2em" },
67
+ { style: "body{ font-size: 1.2em; }", expected: "font-size:1.2em" },
68
68
  ];
69
69
  for (const style of styles) {
70
70
  const cssPath = path.join(publicDir, "styles.min.css");
@@ -13,6 +13,7 @@ import {
13
13
  } from "../model/FileModel.js";
14
14
 
15
15
  import SqliteAdapter from "../model/SqliteAdapter.js";
16
+ import FileAdapter from "../model/FileAdapter.js";
16
17
 
17
18
  function generateRandomContent(length) {
18
19
  let str = "";
@@ -25,44 +26,6 @@ function generateRandomContent(length) {
25
26
  return str;
26
27
  }
27
28
 
28
- describe("File Model test", () => {
29
- it("should init files and load info", async () => {
30
- const infoFile = "test_bloginfo.json";
31
- const articlesFile = "test_articles.txt";
32
-
33
- // Ensure clean state
34
- if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
35
- if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
36
-
37
- await initFiles(infoFile, articlesFile);
38
-
39
- const info = await loadInfo(infoFile);
40
- expect(info.title).toBe("Blog");
41
-
42
- // Cleanup
43
- fs.unlinkSync(infoFile);
44
- fs.unlinkSync(articlesFile);
45
- });
46
-
47
- it("should write to articles.txt file", async () => {
48
- const content = generateRandomContent(200);
49
- const article = new Article("hello", content, new Date().toISOString());
50
- const filename = "test_articles.txt";
51
-
52
- await appendArticle(filename, article);
53
- const articles = await loadArticles(filename);
54
-
55
- // Compare properties since 'articles' contains plain objects, not Article instances
56
- expect(articles.length).toBeGreaterThan(0);
57
- const lastArticle = articles[articles.length - 1];
58
- expect(lastArticle.title).toBe(article.title);
59
- expect(lastArticle.content).toBe(article.content);
60
-
61
- // Cleanup
62
- if (fs.existsSync(filename)) fs.unlinkSync(filename);
63
- });
64
- });
65
-
66
29
  describe("API Model test", () => {
67
30
  beforeAll((done) => {
68
31
  const port = 8081;
@@ -127,55 +90,140 @@ describe("API Model test", () => {
127
90
  });
128
91
 
129
92
  describe("Database Model test", () => {
130
- it("should be empty", async () => {
131
- const blog = new Blog();
132
- blog.database.type = "sqlite";
133
- blog.database.dbname = "test";
134
- blog.setStyle(
135
- "body { font-family: Arial, sans-serif; } h1 { color: #333; }"
136
- );
93
+ describe("Sqlite Database Adapter test", () => {
94
+ it("should be empty", async () => {
95
+ const blog = new Blog();
96
+ const sqliteAdapter = new SqliteAdapter({
97
+ dbname: "blog",
98
+ });
99
+ blog.setDatabaseAdapter(sqliteAdapter);
100
+ await blog.init();
101
+
102
+ expect(await blog.toHTML()).not.toContain("<article>");
103
+ expect(blog).toBeDefined();
104
+ });
137
105
 
138
- //const article = new Article("hello", "hello world1!");
139
- //blog.postArticle(article);
106
+ it("should load blog data from sqlite database", async () => {
107
+ const blog = new Blog();
108
+ const sqliteAdapter = new SqliteAdapter({
109
+ dbname: "blog",
110
+ });
111
+ blog.setDatabaseAdapter(sqliteAdapter);
112
+ await blog.init();
113
+
114
+ const content = generateRandomContent(200);
115
+
116
+ await new Promise((resolve) => {
117
+ const req = {
118
+ on: (event, cb) => {
119
+ if (event === "data") {
120
+ setTimeout(() => cb(`title=hello&content=${content}`), 0);
121
+ }
122
+ if (event === "end") {
123
+ setTimeout(() => cb(), 20);
124
+ }
125
+ },
126
+ off: () => {},
127
+ };
128
+ const res = {
129
+ writeHead: () => {},
130
+ end: resolve,
131
+ };
132
+ blog.postArticle(req, res);
133
+ });
134
+
135
+ expect(await blog.toHTML()).toContain(content);
136
+ expect(blog).toBeDefined();
137
+ });
138
+ });
139
+ describe("Postgres Adapter test", () => {});
140
+ describe("File Adapter test", () => {
141
+ it("should update blog title", async () => {
142
+ const title_org = "Test Blog Title";
143
+ const fileAdapter = new FileAdapter();
144
+ await fileAdapter.initialize();
145
+ await fileAdapter.updateBlogTitle(title_org);
146
+ const title = await fileAdapter.getBlogTitle();
147
+ console.log(title);
148
+ expect(title).toBe(title_org);
149
+ });
150
+ });
151
+ describe("File Model test", () => {
152
+ it("should init files and load info", async () => {
153
+ const infoFile = "test_bloginfo.json";
154
+ const articlesFile = "test_articles.txt";
140
155
 
141
- //await blog.load("blog.json");
156
+ // Ensure clean state
157
+ if (fs.existsSync(infoFile)) fs.unlinkSync(infoFile);
158
+ if (fs.existsSync(articlesFile)) fs.unlinkSync(articlesFile);
142
159
 
143
- expect(await blog.toHTML()).not.toContain("<article>");
144
- expect(blog).toBeDefined();
145
- });
160
+ await initFiles(infoFile, articlesFile);
146
161
 
147
- it("should load blog data from sqlite database", async () => {
148
- const blog = new Blog();
149
- const sqliteAdapter = new SqliteAdapter({
150
- dbname: "blog",
162
+ const info = await loadInfo(infoFile);
163
+ expect(info.title).toBe("Blog");
164
+
165
+ // Cleanup
166
+ fs.unlinkSync(infoFile);
167
+ fs.unlinkSync(articlesFile);
151
168
  });
152
- blog.setDatabaseAdapter(sqliteAdapter);
153
- await blog.init();
154
169
 
155
- const content = generateRandomContent(200);
170
+ it("should read and write to articles.txt file", async () => {
171
+ const content = generateRandomContent(200);
172
+ const article = new Article("hello", content, new Date().toISOString());
173
+ const filename = "test_articles.txt";
156
174
 
157
- await new Promise((resolve) => {
158
- const req = {
159
- on: (event, cb) => {
160
- if (event === "data") {
161
- setTimeout(() => cb(`title=hello&content=${content}`), 0);
162
- }
163
- if (event === "end") {
164
- setTimeout(() => cb(), 20);
165
- }
166
- },
167
- off: () => {},
168
- };
169
- const res = {
170
- writeHead: () => {},
171
- end: resolve,
172
- };
173
- blog.postArticle(req, res);
175
+ await appendArticle(filename, article);
176
+ const articles = await loadArticles(filename);
177
+
178
+ // Compare properties since 'articles' contains plain objects, not Article instances
179
+ expect(articles.length).toBeGreaterThan(0);
180
+ const lastArticle = articles[articles.length - 1];
181
+ expect(lastArticle.title).toBe(article.title);
182
+ expect(lastArticle.content).toBe(article.content);
183
+
184
+ // Cleanup
185
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
174
186
  });
175
187
 
176
- //await blog.load("blog.json");
188
+ it("should be empty articles.txt", async () => {
189
+ const filename = "test_articles_empty.txt";
190
+ const articles = await loadArticles(filename);
191
+ expect(articles.length).toBe(0);
192
+ // Cleanup
193
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
194
+ });
177
195
 
178
- expect(await blog.toHTML()).toContain(content);
179
- expect(blog).toBeDefined();
196
+ it("should read and write multiple articles to articles.txt file", async () => {
197
+ const filename = "test_articles.txt";
198
+
199
+ // Ensure clean state
200
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
201
+
202
+ try {
203
+ const articles_org = [];
204
+ const count = 20;
205
+ for (let i = 0; i < count; i++) {
206
+ const title = generateRandomContent(20);
207
+ const content = generateRandomContent(200);
208
+ const article = new Article(title, content, new Date().toISOString());
209
+ articles_org.push(article);
210
+
211
+ await appendArticle(filename, article);
212
+ }
213
+ const articles = await loadArticles(filename);
214
+
215
+ // Compare properties since 'articles' contains plain objects, not Article instances
216
+ expect(articles).toHaveLength(count);
217
+
218
+ for (let i = 0; i < articles.length; i++) {
219
+ expect(articles[i].title).toBe(articles_org[i].title);
220
+ expect(articles[i].content).toBe(articles_org[i].content);
221
+ expect(articles[i].createdAt).toBe(articles_org[i].createdAt);
222
+ }
223
+ } finally {
224
+ // Cleanup
225
+ if (fs.existsSync(filename)) fs.unlinkSync(filename);
226
+ }
227
+ });
180
228
  });
181
229
  });
@@ -16,10 +16,26 @@ describe("Blog Stylesheet Test", () => {
16
16
  expect(data).toContain("body");
17
17
  expect(data).toContain("nav a");
18
18
  expect(data).toContain(".datetime");
19
- expect(data).toContain("font-style: normal");
20
19
  expect(data).toContain("color: darkgray");
21
20
  });
22
21
 
22
+ it("should load the stylesheet (.css) file from public", async () => {
23
+ const filepath = path.join(__dirname, "../public/styles.min.css");
24
+
25
+ const data = await fs.promises.readFile(filepath, "utf8");
26
+ console.log(data);
27
+ expect(data).toContain("font-family:Arial");
28
+ expect(data).toContain("h1");
29
+ expect(data).toContain(".grid{");
30
+ expect(data).toContain(".grid article");
31
+ expect(data).toContain("nav a");
32
+ expect(data).toContain(".datetime");
33
+ expect(data).toContain("nav a:visited{");
34
+ expect(data).toContain("@media screen");
35
+ expect(data).toContain("#wrapper{");
36
+ expect(data).not.toContain("color:darkgray");
37
+ });
38
+
23
39
  it("should load and compile the stylesheet (.css) correctly", async () => {
24
40
  const blog = new Blog();
25
41
  blog.title = "My Blog";
@@ -38,7 +54,7 @@ describe("Blog Stylesheet Test", () => {
38
54
  expect(publicCSS).toContain("body");
39
55
  expect(publicCSS).toContain("nav a");
40
56
  expect(publicCSS).toContain(".datetime");
41
- expect(publicCSS).toContain("font-style: normal");
57
+ expect(publicCSS).toContain("font-style:normal");
42
58
  expect(publicCSS).toContain("color:");
43
59
  });
44
60
 
@@ -60,7 +76,7 @@ describe("Blog Stylesheet Test", () => {
60
76
  expect(publicCSS).toContain("body");
61
77
  expect(publicCSS).toContain("nav a");
62
78
  expect(publicCSS).toContain(".datetime");
63
- expect(publicCSS).toContain("font-style: normal");
79
+ expect(publicCSS).toContain("font-style:normal");
64
80
  expect(publicCSS).toContain("color:");
65
81
  });
66
82
  });