chore: more refactorings and optimizations
This commit is contained in:
72
package-lock.json
generated
72
package-lock.json
generated
@@ -178,7 +178,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -751,7 +750,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz",
|
||||
"integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
@@ -828,7 +826,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
|
||||
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
@@ -850,7 +847,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.13.tgz",
|
||||
"integrity": "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -946,7 +942,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
@@ -987,7 +982,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
@@ -1382,6 +1376,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -1403,6 +1398,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -1419,6 +1415,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -1433,6 +1430,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -3980,7 +3978,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.0.tgz",
|
||||
"integrity": "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@libsql/core": "^0.17.0",
|
||||
"@libsql/hrana-client": "^0.9.0",
|
||||
@@ -5187,7 +5184,8 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
@@ -5416,7 +5414,6 @@
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -5427,7 +5424,6 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -5535,7 +5531,6 @@
|
||||
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.56.0",
|
||||
"@typescript-eslint/types": "8.56.0",
|
||||
@@ -5915,7 +5910,6 @@
|
||||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.18",
|
||||
"fflate": "^0.8.2",
|
||||
@@ -6102,7 +6096,6 @@
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6136,7 +6129,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -6659,7 +6651,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -7355,7 +7346,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.1.0",
|
||||
@@ -7492,8 +7484,7 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-cloud": {
|
||||
"version": "1.2.8",
|
||||
@@ -7771,7 +7762,6 @@
|
||||
"integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "26.7.0",
|
||||
"builder-util": "26.4.1",
|
||||
@@ -7873,7 +7863,8 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
@@ -8346,6 +8337,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -8366,6 +8358,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -8392,6 +8385,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encoding": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
@@ -8499,7 +8502,6 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -8577,7 +8579,6 @@
|
||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -10085,7 +10086,6 @@
|
||||
"integrity": "sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.31",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@@ -10421,6 +10421,7 @@
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
@@ -11690,6 +11691,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -11702,7 +11704,6 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@@ -12264,7 +12265,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12340,6 +12340,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -12357,6 +12358,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -12377,6 +12379,7 @@
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
@@ -12392,6 +12395,7 @@
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -12546,7 +12550,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
@@ -12580,7 +12583,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
@@ -12614,7 +12616,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz",
|
||||
"integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
@@ -12692,7 +12693,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -12758,7 +12758,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -12788,7 +12787,8 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
@@ -13093,6 +13093,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -13835,6 +13836,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -14114,8 +14116,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"devOptional": true,
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
@@ -14665,7 +14666,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -14873,7 +14873,6 @@
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@@ -14956,7 +14955,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -15516,7 +15514,6 @@
|
||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.18",
|
||||
"@vitest/mocker": "4.0.18",
|
||||
@@ -15594,7 +15591,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz",
|
||||
"integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.28",
|
||||
"@vue/compiler-sfc": "3.5.28",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getPicoStylesheetHref, sanitizePicoTheme, type PicoThemeName } from '..
|
||||
import type { MenuDocument } from './MenuEngine';
|
||||
import type { ProjectMetadata } from './MetaEngine';
|
||||
import { loadPublishedGenerationSets } from './GenerationPostSnapshotService';
|
||||
import { buildSitemapAndFeeds } from './GenerationSitemapFeedService';
|
||||
import { buildSitemapAndFeeds, collectSitemapArchiveMetadata } from './GenerationSitemapFeedService';
|
||||
import { buildTargetedValidationPlan, planMissingValidationPaths } from './ValidationApplyPlannerService';
|
||||
import { compareSitemapToHtml } from './SiteValidationDiffService';
|
||||
import {
|
||||
@@ -218,30 +218,57 @@ export class BlogGenerationEngine {
|
||||
|
||||
const generationPostIndex = buildGenerationPostIndex(publishedListPosts);
|
||||
|
||||
onProgress(5, 'Building sitemap XML...');
|
||||
const {
|
||||
allTags,
|
||||
allCategories,
|
||||
yearMonths,
|
||||
years,
|
||||
yearMonthDays,
|
||||
urls,
|
||||
sitemapXml,
|
||||
rssXml,
|
||||
atomXml,
|
||||
feedPosts,
|
||||
} = buildSitemapAndFeeds({
|
||||
baseUrl: options.baseUrl,
|
||||
projectName: options.projectName,
|
||||
projectDescription: options.projectDescription,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
postIndex: generationPostIndex,
|
||||
includeFeeds: true,
|
||||
});
|
||||
let allTags = new Set<string>();
|
||||
let allCategories = new Set<string>();
|
||||
let yearMonths = new Map<string, Date>();
|
||||
let years = new Map<number, Date>();
|
||||
let yearMonthDays = new Map<string, Date>();
|
||||
let urls: string[] = [];
|
||||
let sitemapXml = '';
|
||||
let rssXml = '';
|
||||
let atomXml = '';
|
||||
let feedPosts: PostData[] = [];
|
||||
|
||||
onProgress(8, 'Building RSS and Atom feeds...');
|
||||
if (includeCore) {
|
||||
onProgress(5, 'Building sitemap XML...');
|
||||
const sitemapAndFeedResult = buildSitemapAndFeeds({
|
||||
baseUrl: options.baseUrl,
|
||||
projectName: options.projectName,
|
||||
projectDescription: options.projectDescription,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
postIndex: generationPostIndex,
|
||||
includeFeeds: true,
|
||||
});
|
||||
|
||||
allTags = sitemapAndFeedResult.allTags;
|
||||
allCategories = sitemapAndFeedResult.allCategories;
|
||||
yearMonths = sitemapAndFeedResult.yearMonths;
|
||||
years = sitemapAndFeedResult.years;
|
||||
yearMonthDays = sitemapAndFeedResult.yearMonthDays;
|
||||
urls = sitemapAndFeedResult.urls;
|
||||
sitemapXml = sitemapAndFeedResult.sitemapXml;
|
||||
rssXml = sitemapAndFeedResult.rssXml;
|
||||
atomXml = sitemapAndFeedResult.atomXml;
|
||||
feedPosts = sitemapAndFeedResult.feedPosts;
|
||||
|
||||
onProgress(8, 'Building RSS and Atom feeds...');
|
||||
} else if (includeCategory || includeTag || includeDate) {
|
||||
const archiveMetadata = collectSitemapArchiveMetadata({
|
||||
baseUrl: options.baseUrl,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
});
|
||||
|
||||
allTags = archiveMetadata.allTags;
|
||||
allCategories = archiveMetadata.allCategories;
|
||||
yearMonths = archiveMetadata.yearMonths;
|
||||
years = archiveMetadata.years;
|
||||
yearMonthDays = archiveMetadata.yearMonthDays;
|
||||
feedPosts = archiveMetadata.feedPosts;
|
||||
}
|
||||
|
||||
const htmlDir = path.join(options.dataDir, 'html');
|
||||
await fs.mkdir(htmlDir, { recursive: true });
|
||||
@@ -321,11 +348,16 @@ export class BlogGenerationEngine {
|
||||
},
|
||||
});
|
||||
|
||||
const knownOutputDirectories = new Set<string>();
|
||||
const generatedHashCache = new Map<string, string | null>();
|
||||
|
||||
const writePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({
|
||||
projectId,
|
||||
htmlDir,
|
||||
urlPath,
|
||||
content,
|
||||
knownDirectories: knownOutputDirectories,
|
||||
hashCache: generatedHashCache,
|
||||
});
|
||||
|
||||
let pagesGenerated = 0;
|
||||
|
||||
@@ -33,6 +33,7 @@ export async function writeFileIfHashChanged(params: {
|
||||
filePath: string;
|
||||
relativePath: string;
|
||||
content: string;
|
||||
hashCache?: Map<string, string | null>;
|
||||
getGeneratedFileHash?: (projectId: string, relativePath: string) => Promise<string | null>;
|
||||
setGeneratedFileHash?: (projectId: string, relativePath: string, hash: string) => Promise<void>;
|
||||
computeHash?: (content: string) => string;
|
||||
@@ -42,13 +43,21 @@ export async function writeFileIfHashChanged(params: {
|
||||
const hashFn = params.computeHash ?? computeContentHash;
|
||||
|
||||
const hash = hashFn(params.content);
|
||||
const previousHash = await getHash(params.projectId, params.relativePath);
|
||||
let previousHash: string | null;
|
||||
if (params.hashCache && params.hashCache.has(params.relativePath)) {
|
||||
previousHash = params.hashCache.get(params.relativePath) ?? null;
|
||||
} else {
|
||||
previousHash = await getHash(params.projectId, params.relativePath);
|
||||
params.hashCache?.set(params.relativePath, previousHash);
|
||||
}
|
||||
|
||||
if (previousHash === hash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await fs.writeFile(params.filePath, params.content, 'utf-8');
|
||||
await setHash(params.projectId, params.relativePath, hash);
|
||||
params.hashCache?.set(params.relativePath, hash);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,6 +66,9 @@ export async function writeHtmlPage(params: {
|
||||
htmlDir: string;
|
||||
urlPath: string;
|
||||
content: string;
|
||||
knownDirectories?: Set<string>;
|
||||
hashCache?: Map<string, string | null>;
|
||||
ensureDirectory?: (dirPath: string) => Promise<void>;
|
||||
getGeneratedFileHash?: (projectId: string, relativePath: string) => Promise<string | null>;
|
||||
setGeneratedFileHash?: (projectId: string, relativePath: string, hash: string) => Promise<void>;
|
||||
computeHash?: (content: string) => string;
|
||||
@@ -66,14 +78,26 @@ export async function writeHtmlPage(params: {
|
||||
? path.join(params.htmlDir, normalizedPath, 'index.html')
|
||||
: path.join(params.htmlDir, 'index.html');
|
||||
const relativePath = normalizedPath ? `${normalizedPath}/index.html` : 'index.html';
|
||||
const directoryPath = path.dirname(filePath);
|
||||
const ensureDirectory = params.ensureDirectory ?? (async (dirPath: string) => {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
});
|
||||
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
if (params.knownDirectories) {
|
||||
if (!params.knownDirectories.has(directoryPath)) {
|
||||
await ensureDirectory(directoryPath);
|
||||
params.knownDirectories.add(directoryPath);
|
||||
}
|
||||
} else {
|
||||
await ensureDirectory(directoryPath);
|
||||
}
|
||||
|
||||
return writeFileIfHashChanged({
|
||||
projectId: params.projectId,
|
||||
filePath,
|
||||
relativePath,
|
||||
content: params.content,
|
||||
hashCache: params.hashCache,
|
||||
getGeneratedFileHash: params.getGeneratedFileHash,
|
||||
setGeneratedFileHash: params.setGeneratedFileHash,
|
||||
computeHash: params.computeHash,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CategoryRenderSettings } from './PageRenderer';
|
||||
import { buildCanonicalPostPath } from './PageRenderer';
|
||||
import type { MenuDocument } from './MenuEngine';
|
||||
import type { ProjectMetadata } from './MetaEngine';
|
||||
import type { PostData } from './PostEngine';
|
||||
@@ -15,6 +16,7 @@ interface RenderContext {
|
||||
};
|
||||
metadata?: ProjectMetadata | null;
|
||||
menu?: MenuDocument;
|
||||
skipContextSetup?: boolean;
|
||||
maxPostsPerPage?: number;
|
||||
}
|
||||
|
||||
@@ -89,6 +91,10 @@ export function createPreviewBackedGenerationRouteRenderer(params: {
|
||||
projectDescription: params.options.projectDescription,
|
||||
};
|
||||
|
||||
params.engines.postEngine.setProjectContext(projectContext.projectId, projectContext.dataDir);
|
||||
params.engines.mediaEngine.setProjectContext?.(projectContext.projectId, projectContext.dataDir, projectContext.dataDir);
|
||||
params.engines.postMediaEngine.setProjectContext(projectContext.projectId);
|
||||
|
||||
const mediaItemsPromiseCache = new Map<string, Promise<unknown[]>>();
|
||||
const postsByFilterPromiseCache = new Map<string, Promise<PostData[]>>();
|
||||
const publishedSnapshotByIdPromiseCache = new Map<string, Promise<PostData | null>>();
|
||||
@@ -201,12 +207,42 @@ export function createPreviewBackedGenerationRouteRenderer(params: {
|
||||
getActiveProjectContext: async () => projectContext,
|
||||
});
|
||||
|
||||
const htmlRewriteContextPromise: Promise<{ canonicalPostPathBySlug: Map<string, string>; canonicalMediaPathBySourcePath: Map<string, string> }> = (async () => {
|
||||
const canonicalPostPathBySlug = new Map<string, string>();
|
||||
for (const post of params.publishedPostsForLookup) {
|
||||
canonicalPostPathBySlug.set(post.slug, buildCanonicalPostPath(post));
|
||||
}
|
||||
|
||||
const canonicalMediaPathBySourcePath = new Map<string, string>();
|
||||
const mediaItems = await cachedMediaEngine.getAllMedia();
|
||||
for (const media of mediaItems as Array<{ createdAt: Date; filename: string; originalName: string }>) {
|
||||
const year = media.createdAt.getFullYear();
|
||||
const month = String(media.createdAt.getMonth() + 1).padStart(2, '0');
|
||||
const canonicalPath = `/media/${year}/${month}/${media.filename}`;
|
||||
|
||||
const originalNameKey = `media/${year}/${month}/${media.originalName}`.toLowerCase();
|
||||
const filenameKey = `media/${year}/${month}/${media.filename}`.toLowerCase();
|
||||
|
||||
canonicalMediaPathBySourcePath.set(originalNameKey, canonicalPath);
|
||||
canonicalMediaPathBySourcePath.set(filenameKey, canonicalPath);
|
||||
}
|
||||
|
||||
return {
|
||||
canonicalPostPathBySlug,
|
||||
canonicalMediaPathBySourcePath,
|
||||
};
|
||||
})();
|
||||
|
||||
return createGenerationRouteRenderer({
|
||||
renderWithContext: (pathname, context) => previewServer.renderRouteForContext(pathname, context),
|
||||
renderWithContext: async (pathname, context) => previewServer.renderRouteForContext(pathname, {
|
||||
...context,
|
||||
htmlRewriteContext: await htmlRewriteContextPromise,
|
||||
}),
|
||||
context: {
|
||||
projectContext,
|
||||
metadata,
|
||||
menu,
|
||||
skipContextSetup: true,
|
||||
maxPostsPerPage: params.maxPostsPerPage,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,6 +32,18 @@ export interface SitemapFeedBuildResult {
|
||||
feedPosts: PostData[];
|
||||
}
|
||||
|
||||
export interface SitemapArchiveMetadata {
|
||||
allTags: Set<string>;
|
||||
allCategories: Set<string>;
|
||||
yearMonths: Map<string, Date>;
|
||||
years: Map<number, Date>;
|
||||
yearMonthDays: Map<string, Date>;
|
||||
feedPosts: PostData[];
|
||||
postUrls: Array<{ loc: string; lastmod: string }>;
|
||||
pageUrls: Array<{ loc: string; lastmod: string }>;
|
||||
latestPostUpdatedAt: string;
|
||||
}
|
||||
|
||||
function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
|
||||
if (post.createdAt instanceof Date) {
|
||||
return post.createdAt;
|
||||
@@ -142,19 +154,19 @@ function escapeCdata(value: string): string {
|
||||
return value.replace(/]]>/g, ']]]]><![CDATA[>');
|
||||
}
|
||||
|
||||
export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): SitemapFeedBuildResult {
|
||||
export function collectSitemapArchiveMetadata(params: {
|
||||
baseUrl: string;
|
||||
maxPostsPerPage: number;
|
||||
publishedPosts: PostData[];
|
||||
publishedListPosts: PostData[];
|
||||
}): SitemapArchiveMetadata {
|
||||
const {
|
||||
baseUrl,
|
||||
projectName,
|
||||
projectDescription,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
postIndex,
|
||||
includeFeeds,
|
||||
} = params;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const allTags = new Set<string>();
|
||||
const allCategories = new Set<string>();
|
||||
const yearMonths = new Map<string, Date>();
|
||||
@@ -206,7 +218,53 @@ export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): Sitema
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const latestPostUpdatedAt = publishedListPosts[0]?.updatedAt.toISOString() || now;
|
||||
const feedPosts = publishedListPosts.slice(0, maxPostsPerPage);
|
||||
|
||||
return {
|
||||
allTags,
|
||||
allCategories,
|
||||
yearMonths,
|
||||
years,
|
||||
yearMonthDays,
|
||||
feedPosts,
|
||||
postUrls,
|
||||
pageUrls,
|
||||
latestPostUpdatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): SitemapFeedBuildResult {
|
||||
const {
|
||||
baseUrl,
|
||||
projectName,
|
||||
projectDescription,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
postIndex,
|
||||
includeFeeds,
|
||||
} = params;
|
||||
|
||||
const archiveMetadata = collectSitemapArchiveMetadata({
|
||||
baseUrl,
|
||||
maxPostsPerPage,
|
||||
publishedPosts,
|
||||
publishedListPosts,
|
||||
});
|
||||
|
||||
const {
|
||||
allTags,
|
||||
allCategories,
|
||||
yearMonths,
|
||||
years,
|
||||
yearMonthDays,
|
||||
postUrls,
|
||||
pageUrls,
|
||||
latestPostUpdatedAt,
|
||||
feedPosts,
|
||||
} = archiveMetadata;
|
||||
|
||||
const urls: string[] = [];
|
||||
urls.push(buildSitemapUrl(`${baseUrl}/`, latestPostUpdatedAt, 'daily', '1.0'));
|
||||
@@ -259,7 +317,6 @@ export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): Sitema
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const feedPosts = publishedListPosts.slice(0, maxPostsPerPage);
|
||||
if (!includeFeeds) {
|
||||
return {
|
||||
allTags,
|
||||
|
||||
@@ -155,6 +155,10 @@ function normalizeCategorySettings(value: unknown): Record<string, CategoryRende
|
||||
);
|
||||
}
|
||||
|
||||
function isJsonParseError(error: unknown): boolean {
|
||||
return error instanceof SyntaxError;
|
||||
}
|
||||
|
||||
/**
|
||||
* MetaEngine manages project metadata like available tags and categories.
|
||||
*
|
||||
@@ -447,6 +451,11 @@ export class MetaEngine extends EventEmitter {
|
||||
const parsed = JSON.parse(content) as ProjectMetadata;
|
||||
this.projectMetadata = normalizeProjectMetadata(parsed);
|
||||
} catch (error) {
|
||||
if (isJsonParseError(error)) {
|
||||
console.warn('[MetaEngine] Failed to parse project metadata JSON, using null metadata:', error);
|
||||
this.projectMetadata = null;
|
||||
return;
|
||||
}
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('[MetaEngine] Failed to load project metadata:', error);
|
||||
throw error;
|
||||
@@ -466,6 +475,10 @@ export class MetaEngine extends EventEmitter {
|
||||
const parsed = JSON.parse(content) as Record<string, unknown>;
|
||||
return normalizeCategoryMetadata(parsed);
|
||||
} catch (error) {
|
||||
if (isJsonParseError(error)) {
|
||||
console.warn('[MetaEngine] Failed to parse category metadata JSON, using default metadata merge:', error);
|
||||
return null;
|
||||
}
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('[MetaEngine] Failed to load category metadata:', error);
|
||||
throw error;
|
||||
@@ -490,6 +503,11 @@ export class MetaEngine extends EventEmitter {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isJsonParseError(error)) {
|
||||
console.warn('[MetaEngine] Failed to parse categories JSON, treating as empty and rebuilding from DB/defaults:', error);
|
||||
this.categories.clear();
|
||||
return;
|
||||
}
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
console.error('[MetaEngine] Failed to load categories:', error);
|
||||
throw error;
|
||||
@@ -653,6 +671,18 @@ export class MetaEngine extends EventEmitter {
|
||||
// Handle project metadata
|
||||
if (projectMetadataFileExists) {
|
||||
await this.loadProjectMetadata();
|
||||
if (!this.projectMetadata) {
|
||||
const projectData = await this.fetchProjectFromDatabase();
|
||||
if (!projectData) {
|
||||
throw new Error(`Project not found in database: ${this.currentProjectId}`);
|
||||
}
|
||||
this.projectMetadata = {
|
||||
name: projectData.name,
|
||||
description: projectData.description || undefined,
|
||||
maxPostsPerPage: DEFAULT_MAX_POSTS_PER_PAGE,
|
||||
};
|
||||
await this.saveProjectMetadata();
|
||||
}
|
||||
if (this.projectMetadata?.dataPath !== undefined) {
|
||||
const { dataPath: _dataPath, ...metadataWithoutDataPath } = this.projectMetadata;
|
||||
this.projectMetadata = metadataWithoutDataPath;
|
||||
|
||||
@@ -166,6 +166,8 @@ export class PreviewServer {
|
||||
projectContext: ActiveProjectContext;
|
||||
metadata?: ProjectMetadata | null;
|
||||
menu?: MenuDocument;
|
||||
htmlRewriteContext?: HtmlRewriteContext;
|
||||
skipContextSetup?: boolean;
|
||||
maxPostsPerPage?: number;
|
||||
requestTheme?: string | null;
|
||||
htmlThemeAttribute?: string;
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface SharedRouteRenderOptions {
|
||||
projectContext: SharedActiveProjectContext;
|
||||
metadata?: ProjectMetadata | null;
|
||||
menu?: MenuDocument;
|
||||
htmlRewriteContext?: HtmlRewriteContext;
|
||||
skipContextSetup?: boolean;
|
||||
maxPostsPerPage?: number;
|
||||
requestTheme?: string | null;
|
||||
htmlThemeAttribute?: string;
|
||||
@@ -267,11 +269,13 @@ export async function renderRouteWithSharedContext<TCategoryMetadata>(
|
||||
options: SharedRouteRenderOptions,
|
||||
services: SharedRouteRenderServices<TCategoryMetadata>,
|
||||
): Promise<string | null> {
|
||||
services.postEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
services.mediaEngine.setProjectContext?.(options.projectContext.projectId, options.projectContext.dataDir, options.projectContext.dataDir);
|
||||
services.postMediaEngine.setProjectContext(options.projectContext.projectId);
|
||||
services.settingsEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
services.menuEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
if (!options.skipContextSetup) {
|
||||
services.postEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
services.mediaEngine.setProjectContext?.(options.projectContext.projectId, options.projectContext.dataDir, options.projectContext.dataDir);
|
||||
services.postMediaEngine.setProjectContext(options.projectContext.projectId);
|
||||
services.settingsEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
services.menuEngine.setProjectContext(options.projectContext.projectId, options.projectContext.dataDir);
|
||||
}
|
||||
|
||||
let metadata = options.metadata;
|
||||
if (metadata === undefined) {
|
||||
@@ -292,7 +296,7 @@ export async function renderRouteWithSharedContext<TCategoryMetadata>(
|
||||
const appliedTheme = sanitizePicoTheme(options.requestTheme)
|
||||
?? sanitizePicoTheme((metadata as { picoTheme?: unknown } | null)?.picoTheme);
|
||||
const picoStylesheetHref = getPicoStylesheetHref(appliedTheme);
|
||||
const htmlRewriteContext = await services.buildHtmlRewriteContext();
|
||||
const htmlRewriteContext = options.htmlRewriteContext ?? await services.buildHtmlRewriteContext();
|
||||
const normalizedPathname = decodeURIComponent(pathname.replace(/\/+$/, '') || '/');
|
||||
|
||||
return resolveRouteWithSharedServices(normalizedPathname, maxPostsPerPage, htmlRewriteContext, {
|
||||
|
||||
@@ -640,6 +640,118 @@ describe('BlogGenerationEngine', () => {
|
||||
expect(filteredCallCount).toBeLessThanOrEqual(8);
|
||||
});
|
||||
|
||||
it('skips core sitemap and feed build phases for single-only generation', async () => {
|
||||
const posts: PostData[] = [];
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
posts.push(makePost({
|
||||
id: `single-phase-${i}`,
|
||||
slug: `single-phase-${i}`,
|
||||
createdAt: new Date(`2025-${String(i + 1).padStart(2, '0')}-10T10:00:00Z`),
|
||||
}));
|
||||
}
|
||||
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
dataDir: tempDir,
|
||||
baseUrl: 'https://example.com',
|
||||
sections: ['single'],
|
||||
}, onProgress);
|
||||
|
||||
const progressMessages = onProgress.mock.calls.map((call) => String(call[1] ?? ''));
|
||||
expect(progressMessages).not.toContain('Building sitemap XML...');
|
||||
expect(progressMessages).not.toContain('Building RSS and Atom feeds...');
|
||||
expect(progressMessages).not.toContain('Writing sitemap and feeds...');
|
||||
});
|
||||
|
||||
it('skips sitemap XML build phase for archive-only generation sections', async () => {
|
||||
const posts: PostData[] = [];
|
||||
for (let i = 0; i < 8; i += 1) {
|
||||
posts.push(makePost({
|
||||
id: `archive-only-${i}`,
|
||||
slug: `archive-only-${i}`,
|
||||
categories: [`cat-${i % 2}`],
|
||||
tags: [`tag-${i % 3}`],
|
||||
createdAt: new Date(`2025-${String((i % 4) + 1).padStart(2, '0')}-10T10:00:00Z`),
|
||||
}));
|
||||
}
|
||||
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
const onProgress = vi.fn();
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
dataDir: tempDir,
|
||||
baseUrl: 'https://example.com',
|
||||
sections: ['category', 'tag', 'date'],
|
||||
}, onProgress);
|
||||
|
||||
const progressMessages = onProgress.mock.calls.map((call) => String(call[1] ?? ''));
|
||||
expect(progressMessages).not.toContain('Building sitemap XML...');
|
||||
expect(progressMessages).not.toContain('Building RSS and Atom feeds...');
|
||||
expect(progressMessages).not.toContain('Writing sitemap and feeds...');
|
||||
});
|
||||
|
||||
it('does not rebuild canonical rewrite context for every generated html file', async () => {
|
||||
const posts = [
|
||||
makePost({ id: '1', slug: 'p1', categories: ['news'], tags: ['t1'], createdAt: new Date('2025-01-15T10:00:00Z') }),
|
||||
makePost({ id: '2', slug: 'p2', categories: ['news'], tags: ['t2'], createdAt: new Date('2025-01-14T10:00:00Z') }),
|
||||
makePost({ id: '3', slug: 'p3', categories: ['news'], tags: ['t3'], createdAt: new Date('2025-01-13T10:00:00Z') }),
|
||||
];
|
||||
|
||||
setupPosts(posts);
|
||||
|
||||
const pageRendererModule = await import('../../src/main/engine/PageRenderer');
|
||||
const canonicalPathSpy = vi.spyOn(pageRendererModule, 'buildCanonicalPostPath');
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
dataDir: tempDir,
|
||||
baseUrl: 'https://example.com',
|
||||
maxPostsPerPage: 1,
|
||||
}, vi.fn());
|
||||
|
||||
expect(canonicalPathSpy.mock.calls.length).toBeLessThanOrEqual(6);
|
||||
canonicalPathSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not re-setup engine project context for every rendered html file', async () => {
|
||||
const posts = [
|
||||
makePost({ id: '1', slug: 'ctx-1', categories: ['news'], tags: ['t1'], createdAt: new Date('2025-01-15T10:00:00Z') }),
|
||||
makePost({ id: '2', slug: 'ctx-2', categories: ['news'], tags: ['t2'], createdAt: new Date('2025-01-14T10:00:00Z') }),
|
||||
makePost({ id: '3', slug: 'ctx-3', categories: ['news'], tags: ['t3'], createdAt: new Date('2025-01-13T10:00:00Z') }),
|
||||
];
|
||||
|
||||
setupPosts(posts);
|
||||
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
const engine = new BlogGenerationEngine();
|
||||
|
||||
await engine.generate({
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
dataDir: tempDir,
|
||||
baseUrl: 'https://example.com',
|
||||
maxPostsPerPage: 1,
|
||||
}, vi.fn());
|
||||
|
||||
expect(mockPostEngine.setProjectContext.mock.calls.length).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('reduces repeated in-memory filtering across category tag and date generation', async () => {
|
||||
const posts: PostData[] = [];
|
||||
for (let i = 0; i < 30; i += 1) {
|
||||
|
||||
@@ -72,4 +72,77 @@ describe('BlogGenerationOutputService', () => {
|
||||
const saved = await readFile(path.join(htmlDir, 'section', 'page', 'index.html'), 'utf-8');
|
||||
expect(saved).toBe('<html/>');
|
||||
});
|
||||
|
||||
it('reuses in-run hash cache to avoid repeated hash reads for same file', async () => {
|
||||
const tempRoot = path.join('/tmp', makeTempName());
|
||||
await mkdir(tempRoot, { recursive: true });
|
||||
const filePath = path.join(tempRoot, 'cached.txt');
|
||||
const hashCache = new Map<string, string | null>();
|
||||
|
||||
const getHash = vi.fn().mockResolvedValue(null);
|
||||
const setHash = vi.fn().mockResolvedValue(undefined);
|
||||
const hashFn = vi.fn().mockReturnValue('h1');
|
||||
|
||||
await writeFileIfHashChanged({
|
||||
projectId: 'p',
|
||||
filePath,
|
||||
relativePath: 'cached.txt',
|
||||
content: 'hello',
|
||||
getGeneratedFileHash: getHash,
|
||||
setGeneratedFileHash: setHash,
|
||||
computeHash: hashFn,
|
||||
hashCache,
|
||||
});
|
||||
|
||||
await writeFileIfHashChanged({
|
||||
projectId: 'p',
|
||||
filePath,
|
||||
relativePath: 'cached.txt',
|
||||
content: 'hello',
|
||||
getGeneratedFileHash: getHash,
|
||||
setGeneratedFileHash: setHash,
|
||||
computeHash: hashFn,
|
||||
hashCache,
|
||||
});
|
||||
|
||||
expect(getHash).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('avoids repeated directory ensure calls when known directory cache is provided', async () => {
|
||||
const tempRoot = path.join('/tmp', makeTempName());
|
||||
const htmlDir = path.join(tempRoot, 'html');
|
||||
await mkdir(htmlDir, { recursive: true });
|
||||
|
||||
const ensureDirectory = vi.fn(async (dirPath: string) => {
|
||||
await mkdir(dirPath, { recursive: true });
|
||||
});
|
||||
|
||||
const knownDirectories = new Set<string>();
|
||||
|
||||
await writeHtmlPage({
|
||||
projectId: 'p',
|
||||
htmlDir,
|
||||
urlPath: 'section/page',
|
||||
content: '<html/>',
|
||||
getGeneratedFileHash: async () => null,
|
||||
setGeneratedFileHash: async () => undefined,
|
||||
computeHash: () => 'h',
|
||||
ensureDirectory,
|
||||
knownDirectories,
|
||||
});
|
||||
|
||||
await writeHtmlPage({
|
||||
projectId: 'p',
|
||||
htmlDir,
|
||||
urlPath: 'section/page',
|
||||
content: '<html/>',
|
||||
getGeneratedFileHash: async () => 'h',
|
||||
setGeneratedFileHash: async () => undefined,
|
||||
computeHash: () => 'h',
|
||||
ensureDirectory,
|
||||
knownDirectories,
|
||||
});
|
||||
|
||||
expect(ensureDirectory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -861,6 +861,47 @@ describe('MetaEngine', () => {
|
||||
expect(metadata?.categoryMetadata?.news?.title).toBe('Newsroom');
|
||||
});
|
||||
|
||||
it('should continue syncOnStartup when categories.json is malformed and recover from database categories', async () => {
|
||||
const metaDir = metaEngine.getMetaDir();
|
||||
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
|
||||
name: 'Synced Project',
|
||||
}));
|
||||
mockFiles.set(normalizePath(`${metaDir}/categories.json`), '["news",');
|
||||
|
||||
mockPosts = [
|
||||
{ categories: JSON.stringify(['db-cat']) },
|
||||
];
|
||||
|
||||
await expect(metaEngine.syncOnStartup()).resolves.toBeUndefined();
|
||||
|
||||
const categories = await metaEngine.getCategories();
|
||||
expect(categories).toContain('db-cat');
|
||||
|
||||
const metadata = await metaEngine.getProjectMetadata();
|
||||
expect(metadata?.name).toBe('Synced Project');
|
||||
});
|
||||
|
||||
it('should continue syncOnStartup when category-meta.json is malformed and keep valid project metadata', async () => {
|
||||
const metaDir = metaEngine.getMetaDir();
|
||||
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
|
||||
name: 'Synced Project',
|
||||
}));
|
||||
mockFiles.set(normalizePath(`${metaDir}/categories.json`), JSON.stringify(['news']));
|
||||
mockFiles.set(normalizePath(`${metaDir}/category-meta.json`), '{"news":');
|
||||
|
||||
await expect(metaEngine.syncOnStartup()).resolves.toBeUndefined();
|
||||
|
||||
const metadata = await metaEngine.getProjectMetadata() as any;
|
||||
expect(metadata?.name).toBe('Synced Project');
|
||||
expect(metadata?.categoryMetadata?.news).toEqual(
|
||||
expect.objectContaining({
|
||||
renderInLists: true,
|
||||
showTitle: true,
|
||||
title: 'news',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create project.json with data from database during syncOnStartup if file does not exist', async () => {
|
||||
const metaDir = metaEngine.getMetaDir();
|
||||
const projectPath = normalizePath(`${metaDir}/project.json`);
|
||||
|
||||
Reference in New Issue
Block a user