From 0cf13ca559d6f1a693dcadc182e459174017aac2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:27:09 +0000 Subject: [PATCH 001/108] fix(deps): update dependency mind-elixir to v4.3.6 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78f8e3bef..734d7a998 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ "marked": "15.0.6", "mermaid": "11.4.1", "mime-types": "2.1.35", - "mind-elixir": "4.3.5", + "mind-elixir": "4.3.6", "multer": "1.4.5-lts.1", "normalize-strings": "1.1.1", "normalize.css": "8.0.1", @@ -12866,9 +12866,9 @@ } }, "node_modules/mind-elixir": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.5.tgz", - "integrity": "sha512-I1Mxc/jCwHEDMecDjQVpc+WShmzrEnIv6+MnWPauJ0LAiOXMBQB/wpKqlF4bTp+kCqzOHMYryAvIWm0jvloZ8Q==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.6.tgz", + "integrity": "sha512-6E9DT5vOYJ7DMDFXJlAnKU3Q6ekwBkR48Tjo6PchEcxJjPURJsiIASxtIeZCfvp8V39N4WyIa3Yt7Q/SFQkVfw==", "license": "MIT" }, "node_modules/minimalistic-assert": { diff --git a/package.json b/package.json index 2f056ed91..d078620b9 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "marked": "15.0.6", "mermaid": "11.4.1", "mime-types": "2.1.35", - "mind-elixir": "4.3.5", + "mind-elixir": "4.3.6", "multer": "1.4.5-lts.1", "normalize-strings": "1.1.1", "normalize.css": "8.0.1", From e3b8de8843f7551ad93d328eede0d25d4b64a377 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:27:20 +0000 Subject: [PATCH 002/108] fix(deps): update dependency ts-loader to v9.5.2 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78f8e3bef..7f480f86b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "stream-throttle": "0.1.3", "striptags": "3.2.0", "tmp": "0.2.3", - "ts-loader": "9.5.1", + "ts-loader": "9.5.2", "turndown": "7.2.0", "unescape": "1.0.1", "vanilla-js-wheel-zoom": "9.0.4", @@ -17057,9 +17057,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", + "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", "license": "MIT", "dependencies": { "chalk": "^4.1.0", diff --git a/package.json b/package.json index 2f056ed91..2e1c8f5bf 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "stream-throttle": "0.1.3", "striptags": "3.2.0", "tmp": "0.2.3", - "ts-loader": "9.5.1", + "ts-loader": "9.5.2", "turndown": "7.2.0", "unescape": "1.0.1", "vanilla-js-wheel-zoom": "9.0.4", From 5268aaee4f0ee67deec498081a09df50d229689f Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 28 Dec 2024 21:33:05 +0100 Subject: [PATCH 003/108] deps: replace csurf with csrf-csrf --- package-lock.json | 74 ++++++++++++++++++----------------------------- package.json | 3 +- 2 files changed, 29 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78f8e3bef..5a9d9d3fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "codemirror": "5.65.18", "compression": "1.7.5", "cookie-parser": "1.4.7", - "csurf": "1.11.0", + "csrf-csrf": "3.1.0", "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", @@ -117,7 +117,6 @@ "@types/cls-hooked": "4.3.9", "@types/compression": "1.7.5", "@types/cookie-parser": "1.4.8", - "@types/csurf": "1.11.5", "@types/debounce": "1.2.4", "@types/ejs": "3.1.5", "@types/electron-squirrel-startup": "1.0.2", @@ -3807,16 +3806,6 @@ "@types/express": "*" } }, - "node_modules/@types/csurf": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.5.tgz", - "integrity": "sha512-5rw87+5YGixyL2W8wblSUl5DSZi5YOlXE6Awwn2ofLvqKr/1LruKffrQipeJKUX44VaxKj8m5es3vfhltJTOoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*" - } - }, "node_modules/@types/d3": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", @@ -6922,19 +6911,40 @@ "node": ">=12.10" } }, - "node_modules/csrf": { + "node_modules/csrf-csrf": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", - "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", + "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-3.1.0.tgz", + "integrity": "sha512-kZacFfFbdYFxNnFdigRHCzVAq019vJyUUtgPLjCtzh6jMXcWmf8bGUx/hsqtSEMXaNcPm8iXpjC+hW5aeOsRMg==", + "license": "ISC", "dependencies": { - "rndm": "1.2.0", - "tsscmp": "1.0.6", - "uid-safe": "2.1.5" + "http-errors": "^2.0.0" + } + }, + "node_modules/csrf-csrf/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" } }, + "node_modules/csrf-csrf/node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -6976,29 +6986,6 @@ "node": ">=18" } }, - "node_modules/csurf": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", - "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", - "deprecated": "Please use another csrf package", - "dependencies": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "csrf": "3.1.0", - "http-errors": "~1.7.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/csurf/node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cytoscape": { "version": "3.30.4", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz", @@ -15404,11 +15391,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rndm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", - "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" - }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", diff --git a/package.json b/package.json index 2f056ed91..245e9bdd1 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "codemirror": "5.65.18", "compression": "1.7.5", "cookie-parser": "1.4.7", - "csurf": "1.11.0", + "csrf-csrf": "3.1.0", "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", @@ -159,7 +159,6 @@ "@types/cls-hooked": "4.3.9", "@types/compression": "1.7.5", "@types/cookie-parser": "1.4.8", - "@types/csurf": "1.11.5", "@types/debounce": "1.2.4", "@types/ejs": "3.1.5", "@types/electron-squirrel-startup": "1.0.2", From b7876107174bc364662ebdf7d1447aa4134b35d8 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 30 Dec 2024 15:31:29 +0100 Subject: [PATCH 004/108] refactor: replace csurf with csrf-csrf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I've kept the identical same settings as before – however they are not *ideal* from what I read. More secure settings will need to be tested a bit more thoroughly first and will be a separate PR. --- src/routes/routes.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 81cec2c6d..63d85183d 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -9,7 +9,7 @@ import auth from "../services/auth.js"; import cls from "../services/cls.js"; import sql from "../services/sql.js"; import entityChangesService from "../services/entity_changes.js"; -import csurf from "csurf"; +import { doubleCsrf } from "csrf-csrf"; import { createPartialContentHandler } from "@triliumnext/express-partial-content"; import rateLimit from "express-rate-limit"; import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js"; @@ -71,10 +71,15 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js"; import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; -const csrfMiddleware = csurf({ - cookie: { - path: "" // empty, so cookie is valid only for the current path - } +const { doubleCsrfProtection: csrfMiddleware } = doubleCsrf({ + getSecret: (req) => req.secret, + cookieOptions: { + path: "", // empty, so cookie is valid only for the current path + secure: false, + sameSite: false, + httpOnly: false, + }, + cookieName: "_csrf", }); const MAX_ALLOWED_FILE_SIZE_MB = 250; From d20a3bab2acf0f33019d31756cb9e7365850482b Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 5 Jan 2025 19:27:11 +0100 Subject: [PATCH 005/108] fix(csrfMiddleware): use sessionSecret instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit since `cookie-parser` is not configured with a secret, req.secret is not set and hence is `undefined`, which then is used as literal 'undefined' in the hashing function – making it less secure. Instead we can use the existing sessionSecret: the `csrf-csrf` developer confirmed in their Discord chat, that it would be ok to use the same secret here. --- src/routes/routes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 63d85183d..f4f9314fe 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -15,6 +15,7 @@ import rateLimit from "express-rate-limit"; import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js"; import NotFoundError from "../errors/not_found_error.js"; import ValidationError from "../errors/validation_error.js"; +import sessionSecret from "../services/session_secret.js"; // page routes import setupRoute from "./setup.js"; @@ -72,7 +73,7 @@ import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; const { doubleCsrfProtection: csrfMiddleware } = doubleCsrf({ - getSecret: (req) => req.secret, + getSecret: () => sessionSecret, cookieOptions: { path: "", // empty, so cookie is valid only for the current path secure: false, From c36085e5801f991980f871e154e7c22287571692 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 5 Jan 2025 19:37:28 +0100 Subject: [PATCH 006/108] chore: fix TS warning by type narrowing `req.csrfToken` might be undefined according to `csrf-csrf` provided types, so use type narrowing to make sure it exists, before calling it --- src/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/index.ts b/src/routes/index.ts index 5115630a0..4a26d8387 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -19,7 +19,7 @@ function index(req: Request, res: Response) { const view = !utils.isElectron() && req.cookies["trilium-device"] === "mobile" ? "mobile" : "desktop"; - const csrfToken = req.csrfToken(); + const csrfToken = (typeof req.csrfToken === "function") ? req.csrfToken() : undefined; log.info(`Generated CSRF token ${csrfToken} with secret ${res.getHeader("set-cookie")}`); // We force the page to not be cached since on mobile the CSRF token can be From 59ecc614c227628bbb33003e303c6d2f44a68ba3 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 12 Jan 2025 11:43:41 +0100 Subject: [PATCH 007/108] refactor: call logout route via JS required for csrf-csrf to correctly protect against CSRF, as it required the _csrf cookie AND the x-csrf-token HTTP header, the latter cannot be set via simple Form POST action using "../login" here, because "server" method is automatically prepending all paths with "/api", which we don't want here, as we want "/login" --- src/public/app/components/entrypoints.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/public/app/components/entrypoints.ts b/src/public/app/components/entrypoints.ts index b62cfdeb3..f75198cb7 100644 --- a/src/public/app/components/entrypoints.ts +++ b/src/public/app/components/entrypoints.ts @@ -114,11 +114,9 @@ export default class Entrypoints extends Component { utils.reloadFrontendApp(); } - logoutCommand() { - const $logoutForm = $('
').append($(``)); - - $("body").append($logoutForm); - $logoutForm.trigger("submit"); + async logoutCommand() { + await server.post("../logout"); + window.location.replace(`/login`); } backInNoteHistoryCommand() { From d1bd2d2812ae907979084ae87d0be61c52e5d83e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 12 Jan 2025 13:13:59 +0100 Subject: [PATCH 008/108] refactor(routes/login): remove unused rendering of HTML --- src/routes/login.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/login.ts b/src/routes/login.ts index cbb746c30..7b804896e 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -94,8 +94,7 @@ function verifyPassword(guessedPassword: string) { function logout(req: Request, res: Response) { req.session.regenerate(() => { req.session.loggedIn = false; - - res.redirect("login"); + res.sendStatus(200); }); } From 4cd18441e423265942aa17f9aac79c688c710dc4 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 12 Jan 2025 13:16:26 +0100 Subject: [PATCH 009/108] deps: Update package-lock --- package-lock.json | 52 ----------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a9d9d3fd..2df4756a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10830,42 +10830,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/http-errors/node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -16921,14 +16885,6 @@ "node": ">=8.0" } }, - "node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/token-types": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", @@ -17117,14 +17073,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tsx": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", From ea621ef8e16ec725cac72f55dad2c80573d1e12b Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sun, 12 Jan 2025 13:30:02 +0100 Subject: [PATCH 010/108] chore(prettier): fix code style --- src/routes/routes.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/routes/routes.ts b/src/routes/routes.ts index f4f9314fe..d04718cd5 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -73,14 +73,14 @@ import etapiSpecRoute from "../etapi/spec.js"; import etapiBackupRoute from "../etapi/backup.js"; const { doubleCsrfProtection: csrfMiddleware } = doubleCsrf({ - getSecret: () => sessionSecret, - cookieOptions: { - path: "", // empty, so cookie is valid only for the current path - secure: false, - sameSite: false, - httpOnly: false, - }, - cookieName: "_csrf", + getSecret: () => sessionSecret, + cookieOptions: { + path: "", // empty, so cookie is valid only for the current path + secure: false, + sameSite: false, + httpOnly: false + }, + cookieName: "_csrf" }); const MAX_ALLOWED_FILE_SIZE_MB = 250; From fbfee818b25357ab39cbc2c754cd99ac14accfaf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 09:11:11 +0200 Subject: [PATCH 011/108] fix(ci): directory for e2e tests --- .github/workflows/main-docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 13d57701b..d7370b222 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -68,7 +68,7 @@ jobs: - name: Validate container run output run: | - CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host --name trilium_local ${{ env.TEST_TAG }}) + CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) echo "Container ID: $CONTAINER_ID" - name: Wait for the healthchecks to pass From 61a19d5628dcbbad79c7a608a2be8672db5fcb63 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 15:03:42 +0100 Subject: [PATCH 012/108] refactor(data_dir): add FOLDER_PERMISSION const gets rid of previously "magic number" --- src/services/data_dir.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index b87b00d32..97cc6652f 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -32,11 +32,13 @@ function getAppDataDir() { } const DIR_NAME = "trilium-data"; +const FOLDER_PERMISSIONS = 0o700; + function getTriliumDataDir() { if (process.env.TRILIUM_DATA_DIR) { if (!fs.existsSync(process.env.TRILIUM_DATA_DIR)) { - fs.mkdirSync(process.env.TRILIUM_DATA_DIR, 0o700); + fs.mkdirSync(process.env.TRILIUM_DATA_DIR, FOLDER_PERMISSIONS); } return process.env.TRILIUM_DATA_DIR; @@ -51,7 +53,7 @@ function getTriliumDataDir() { const appDataPath = getAppDataDir() + path.sep + DIR_NAME; if (!fs.existsSync(appDataPath)) { - fs.mkdirSync(appDataPath, 0o700); + fs.mkdirSync(appDataPath, FOLDER_PERMISSIONS); } return appDataPath; From 8826021c63b259a0774494564e3e88f0b618e0c4 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 15:06:42 +0100 Subject: [PATCH 013/108] refactor(data_dir): add createDirIfNotExisting function removes some code duplication --- src/services/data_dir.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 97cc6652f..4e7c0315a 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -34,13 +34,15 @@ function getAppDataDir() { const DIR_NAME = "trilium-data"; const FOLDER_PERMISSIONS = 0o700; +function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) { + if (!fs.existsSync(path)) { + fs.mkdirSync(path, permissionMode); + } +} function getTriliumDataDir() { if (process.env.TRILIUM_DATA_DIR) { - if (!fs.existsSync(process.env.TRILIUM_DATA_DIR)) { - fs.mkdirSync(process.env.TRILIUM_DATA_DIR, FOLDER_PERMISSIONS); - } - + createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); return process.env.TRILIUM_DATA_DIR; } @@ -52,9 +54,7 @@ function getTriliumDataDir() { const appDataPath = getAppDataDir() + path.sep + DIR_NAME; - if (!fs.existsSync(appDataPath)) { - fs.mkdirSync(appDataPath, FOLDER_PERMISSIONS); - } + createDirIfNotExisting(appDataPath); return appDataPath; } From 3481c8ba84133944bfe1d40017f0e27cb61551c9 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 15:45:41 +0100 Subject: [PATCH 014/108] refactor(data_dir): use path.join for safer joins https://nodejs.org/api/path.html#pathjoinpaths --- src/services/data_dir.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 4e7c0315a..050afd078 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -10,7 +10,7 @@ import os from "os"; import fs from "fs"; -import path from "path"; +import { join as pathJoin} from "path"; function getAppDataDir() { let appDataDir = os.homedir(); // fallback if OS is not recognized @@ -46,27 +46,24 @@ function getTriliumDataDir() { return process.env.TRILIUM_DATA_DIR; } - const homePath = os.homedir() + path.sep + DIR_NAME; - + const homePath = pathJoin(os.homedir(), DIR_NAME); if (fs.existsSync(homePath)) { return homePath; } - const appDataPath = getAppDataDir() + path.sep + DIR_NAME; - + const appDataPath = pathJoin(getAppDataDir(), DIR_NAME); createDirIfNotExisting(appDataPath); return appDataPath; } const TRILIUM_DATA_DIR = getTriliumDataDir(); -const DIR_SEP = TRILIUM_DATA_DIR + path.sep; -const DOCUMENT_PATH = process.env.TRILIUM_DOCUMENT_PATH || `${DIR_SEP}document.db`; -const BACKUP_DIR = process.env.TRILIUM_BACKUP_DIR || `${DIR_SEP}backup`; -const LOG_DIR = process.env.TRILIUM_LOG_DIR || `${DIR_SEP}log`; -const ANONYMIZED_DB_DIR = process.env.TRILIUM_ANONYMIZED_DB_DIR || `${DIR_SEP}anonymized-db`; -const CONFIG_INI_PATH = process.env.TRILIUM_CONFIG_INI_PATH || `${DIR_SEP}config.ini`; +const DOCUMENT_PATH = process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"); +const BACKUP_DIR = process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"); +const LOG_DIR = process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"); +const ANONYMIZED_DB_DIR = process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"); +const CONFIG_INI_PATH = process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini"); export default { TRILIUM_DATA_DIR, From 7a1e8714af49a143012eb65344c4a2329547339e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 20:38:37 +0100 Subject: [PATCH 015/108] refactor(data_dir): logically order/split cases in getTriliumDataDir - the blocks now clearly follow the intended logic described in the comments - I renamed the `getAppDataDir` to more specific `getPlatformAppDataDir` --- src/services/data_dir.ts | 61 +++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 050afd078..5b963dae7 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -2,38 +2,38 @@ /* * This file resolves trilium data path in this order of priority: - * - if TRILIUM_DATA_DIR environment variable exists, then its value is used as the path - * - if "trilium-data" dir exists directly in the home dir, then it is used - * - based on OS convention, if the "app data directory" exists, we'll use or create "trilium-data" directory there - * - as a fallback if the previous step fails, we'll use home dir + * - case A) if TRILIUM_DATA_DIR environment variable exists, then its value is used as the path + * - case B) if "trilium-data" dir exists directly in the home dir, then it is used + * - case C) based on OS convention, if the "app data directory" exists, we'll use or create "trilium-data" directory there + * - case D) as a fallback if the previous step fails, we'll use home dir */ import os from "os"; import fs from "fs"; import { join as pathJoin} from "path"; -function getAppDataDir() { - let appDataDir = os.homedir(); // fallback if OS is not recognized - - if (os.platform() === "win32" && process.env.APPDATA) { - appDataDir = process.env.APPDATA; - } else if (os.platform() === "linux") { - appDataDir = `${os.homedir()}/.local/share`; - } else if (os.platform() === "darwin") { - appDataDir = `${os.homedir()}/Library/Application Support`; - } - - if (!fs.existsSync(appDataDir)) { - // expected app data path doesn't exist, let's use fallback - appDataDir = os.homedir(); - } - - return appDataDir; -} - const DIR_NAME = "trilium-data"; const FOLDER_PERMISSIONS = 0o700; +function getPlatformAppDataDir(platform: ReturnType, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) { + + switch(true) { + case platform === "win32" && !!ENV_APPDATA_DIR: + return ENV_APPDATA_DIR; + + case platform === "linux": + return `${os.homedir()}/.local/share`; + + case platform === "darwin": + return `${os.homedir()}/Library/Application Support`; + + default: + // if OS is not recognized + return null; + } + +} + function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) { if (!fs.existsSync(path)) { fs.mkdirSync(path, permissionMode); @@ -41,20 +41,29 @@ function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOL } function getTriliumDataDir() { + // case A if (process.env.TRILIUM_DATA_DIR) { createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); return process.env.TRILIUM_DATA_DIR; } + // case B const homePath = pathJoin(os.homedir(), DIR_NAME); if (fs.existsSync(homePath)) { return homePath; } - const appDataPath = pathJoin(getAppDataDir(), DIR_NAME); - createDirIfNotExisting(appDataPath); + // case C + const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); + if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { + const appDataDirPath = pathJoin(platformAppDataDir, DIR_NAME); + createDirIfNotExisting(appDataDirPath); + return appDataDirPath; + } - return appDataPath; + // case D + createDirIfNotExisting(homePath); + return homePath; } const TRILIUM_DATA_DIR = getTriliumDataDir(); From 759d24855b42f16238d3b8d2fa3b89228f7f6769 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 20:42:03 +0100 Subject: [PATCH 016/108] style(data_dir): fix indentation --- src/services/data_dir.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 5b963dae7..090c334b7 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -35,9 +35,9 @@ function getPlatformAppDataDir(platform: ReturnType, ENV_APP } function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) { - if (!fs.existsSync(path)) { - fs.mkdirSync(path, permissionMode); - } + if (!fs.existsSync(path)) { + fs.mkdirSync(path, permissionMode); + } } function getTriliumDataDir() { From 8b1071c4593b51cc656fed25dff8f0b256ab562c Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 21:17:44 +0100 Subject: [PATCH 017/108] refactor(data_dir): export dirs as frozen readonly object previously exported object allowed the values to be changed accidentally at runtime and buildtime --- src/services/data_dir.ts | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 090c334b7..6b7806628 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -66,19 +66,28 @@ function getTriliumDataDir() { return homePath; } +function getDataDirs(TRILIUM_DATA_DIR: string) { + const dataDirs = { + "TRILIUM_DATA_DIR": + TRILIUM_DATA_DIR, + "DOCUMENT_PATH": + process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"), + "BACKUP_DIR": + process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"), + "LOG_DIR": + process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"), + "ANONYMIZED_DB_DIR": + process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"), + "CONFIG_INI_PATH": + process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini") + } as const + + Object.freeze(dataDirs); + return dataDirs; + +} + const TRILIUM_DATA_DIR = getTriliumDataDir(); +const dataDirs = getDataDirs(TRILIUM_DATA_DIR); -const DOCUMENT_PATH = process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"); -const BACKUP_DIR = process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"); -const LOG_DIR = process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"); -const ANONYMIZED_DB_DIR = process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"); -const CONFIG_INI_PATH = process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini"); - -export default { - TRILIUM_DATA_DIR, - DOCUMENT_PATH, - BACKUP_DIR, - LOG_DIR, - ANONYMIZED_DB_DIR, - CONFIG_INI_PATH -}; +export default dataDirs; \ No newline at end of file From 94b8bcf8c930f04ea98aed209d3970d73e09151c Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 22:08:17 +0100 Subject: [PATCH 018/108] refactor(data_dir): export functions to allow for testing --- src/services/data_dir.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 6b7806628..c896e959e 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -15,7 +15,7 @@ import { join as pathJoin} from "path"; const DIR_NAME = "trilium-data"; const FOLDER_PERMISSIONS = 0o700; -function getPlatformAppDataDir(platform: ReturnType, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) { +export function getPlatformAppDataDir(platform: ReturnType, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) { switch(true) { case platform === "win32" && !!ENV_APPDATA_DIR: @@ -40,7 +40,7 @@ function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOL } } -function getTriliumDataDir() { +export function getTriliumDataDir() { // case A if (process.env.TRILIUM_DATA_DIR) { createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); @@ -66,7 +66,7 @@ function getTriliumDataDir() { return homePath; } -function getDataDirs(TRILIUM_DATA_DIR: string) { +export function getDataDirs(TRILIUM_DATA_DIR: string) { const dataDirs = { "TRILIUM_DATA_DIR": TRILIUM_DATA_DIR, From 63079c093988c2c6ff683dd2adb32e065fc2ad2f Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Fri, 3 Jan 2025 22:08:56 +0100 Subject: [PATCH 019/108] test(data_dir): add tests for getPlatformAppDataDir --- spec-es6/data_dir.spec.ts | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 spec-es6/data_dir.spec.ts diff --git a/spec-es6/data_dir.spec.ts b/spec-es6/data_dir.spec.ts new file mode 100644 index 000000000..7d80a2811 --- /dev/null +++ b/spec-es6/data_dir.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, execute, expect } from "./mini_test.ts"; + +import { getPlatformAppDataDir } from "../src/services/data_dir.ts" + + + +describe("data_dir.ts unit tests", () => { + + describe("#getPlatformAppDataDir", () => { + + type TestCaseGetPlatformAppDataDir = [ + description: string, + fnValue: Parameters, + expectedValueFn: (val: ReturnType) => boolean + ] + const testCases: TestCaseGetPlatformAppDataDir[] = [ + + [ + "w/ unsupported OS it should return 'null'", + ["aix", undefined], + (val) => val === null + ], + + [ + "w/ win32 and no APPDATA set it should return 'null'", + ["win32", undefined], + (val) => val === null + ], + + [ + "w/ win32 and set APPDATA it should return set 'APPDATA'", + ["win32", "AppData"], + (val) => val === "AppData" + ], + + [ + "w/ linux it should return '/.local/share'", + ["linux", undefined], + (val) => val !== null && val.endsWith("/.local/share") + ], + + [ + "w/ linux and wrongly set APPDATA it should ignore APPDATA and return /.local/share", + ["linux", "FakeAppData"], + (val) => val !== null && val.endsWith("/.local/share") + ], + + [ + "w/ darwin it should return /Library/Application Support", + ["darwin", undefined], + (val) => val !== null && val.endsWith("/Library/Application Support") + ], + ]; + + testCases.forEach(testCase => { + const [testDescription, value, isExpected] = testCase; + return it(testDescription, () => { + const actual = getPlatformAppDataDir(...value); + const result = isExpected(actual); + expect(result).toBeTruthy() + + }) + }) + + + }) + + describe("#getTriliumDataDir", () => { + // TODO + }) + + describe("#getDataDirs", () => { + // TODO + }) + +}); + +execute() \ No newline at end of file From e021c0cd0e83096b4357f4e441d7aefc9eae493f Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 4 Jan 2025 00:16:26 +0100 Subject: [PATCH 020/108] test(data_dir): add tests for getDataDirs --- spec-es6/data_dir.spec.ts | 68 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/spec-es6/data_dir.spec.ts b/spec-es6/data_dir.spec.ts index 7d80a2811..70a329ff5 100644 --- a/spec-es6/data_dir.spec.ts +++ b/spec-es6/data_dir.spec.ts @@ -1,12 +1,12 @@ import { describe, it, execute, expect } from "./mini_test.ts"; -import { getPlatformAppDataDir } from "../src/services/data_dir.ts" +import { getPlatformAppDataDir, getDataDirs} from "../src/services/data_dir.ts" describe("data_dir.ts unit tests", () => { - describe("#getPlatformAppDataDir", () => { + describe("#getPlatformAppDataDir()", () => { type TestCaseGetPlatformAppDataDir = [ description: string, @@ -69,8 +69,68 @@ describe("data_dir.ts unit tests", () => { // TODO }) - describe("#getDataDirs", () => { - // TODO + describe("#getDataDirs()", () => { + + const envKeys: Omit, "TRILIUM_DATA_DIR">[] = [ + "DOCUMENT_PATH", + "BACKUP_DIR", + "LOG_DIR", + "ANONYMIZED_DB_DIR", + "CONFIG_INI_PATH", + ]; + + const setMockedEnv = (prefix: string | null) => { + envKeys.forEach(key => { + if (prefix) { + process.env[`TRILIUM_${key}`] = `${prefix}_${key}` + } else { + delete process.env[`TRILIUM_${key}`] + } + }) + }; + + it("w/ process.env values present, it should return an object using values from process.env", () => { + + // set mocked values + const mockValuePrefix = "MOCK"; + setMockedEnv(mockValuePrefix); + + // get result + const result = getDataDirs(`${mockValuePrefix}_TRILIUM_DATA_DIR`); + + for (const key in result) { + expect(result[key]).toEqual(`${mockValuePrefix}_${key}`) + } + }) + + it("w/ NO process.env values present, it should return an object using supplied TRILIUM_DATA_DIR as base", () => { + + // make sure values are undefined + setMockedEnv(null); + + const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR" + const result = getDataDirs(mockDataDir); + + for (const key in result) { + expect(result[key].startsWith(mockDataDir)).toBeTruthy() + } + }) + + it("should ignore attempts to change a property on the returned object", () => { + + // make sure values are undefined + setMockedEnv(null); + + const mockDataDir = "/home/test/MOCK_TRILIUM_DATA_DIR" + const result = getDataDirs(mockDataDir); + + //@ts-expect-error - attempt to change value of readonly property + result.BACKUP_DIR = "attempt to change"; + + for (const key in result) { + expect(result[key].startsWith(mockDataDir)).toBeTruthy() + } + }) }) }); From c47522eb50175f1b63b72876032cb19b54a31e66 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 6 Jan 2025 09:37:25 +0100 Subject: [PATCH 021/108] refactor(data_dir): pass DIR_NAME as argument to getTriliumDir makes it a bit cleaner and easier to test in the future, as it is one thing less that'd need mocking :-) --- src/services/data_dir.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index c896e959e..3e3ec396d 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -40,7 +40,7 @@ function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOL } } -export function getTriliumDataDir() { +export function getTriliumDataDir(dataDirName: string) { // case A if (process.env.TRILIUM_DATA_DIR) { createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); @@ -48,7 +48,7 @@ export function getTriliumDataDir() { } // case B - const homePath = pathJoin(os.homedir(), DIR_NAME); + const homePath = pathJoin(os.homedir(), dataDirName); if (fs.existsSync(homePath)) { return homePath; } @@ -56,7 +56,7 @@ export function getTriliumDataDir() { // case C const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { - const appDataDirPath = pathJoin(platformAppDataDir, DIR_NAME); + const appDataDirPath = pathJoin(platformAppDataDir, dataDirName); createDirIfNotExisting(appDataDirPath); return appDataDirPath; } @@ -87,7 +87,7 @@ export function getDataDirs(TRILIUM_DATA_DIR: string) { } -const TRILIUM_DATA_DIR = getTriliumDataDir(); +const TRILIUM_DATA_DIR = getTriliumDataDir(DIR_NAME); const dataDirs = getDataDirs(TRILIUM_DATA_DIR); export default dataDirs; \ No newline at end of file From 6818b2d54caaeb4d61f5813407725c4199d89907 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 6 Jan 2025 09:42:35 +0100 Subject: [PATCH 022/108] style: move "important" funcs to top of file --- src/services/data_dir.ts | 91 ++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 3e3ec396d..9b8c70ec6 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -15,6 +15,52 @@ import { join as pathJoin} from "path"; const DIR_NAME = "trilium-data"; const FOLDER_PERMISSIONS = 0o700; +export function getTriliumDataDir(dataDirName: string) { + // case A + if (process.env.TRILIUM_DATA_DIR) { + createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); + return process.env.TRILIUM_DATA_DIR; + } + + // case B + const homePath = pathJoin(os.homedir(), dataDirName); + if (fs.existsSync(homePath)) { + return homePath; + } + + // case C + const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); + if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { + const appDataDirPath = pathJoin(platformAppDataDir, dataDirName); + createDirIfNotExisting(appDataDirPath); + return appDataDirPath; + } + + // case D + createDirIfNotExisting(homePath); + return homePath; +} + +export function getDataDirs(TRILIUM_DATA_DIR: string) { + const dataDirs = { + "TRILIUM_DATA_DIR": + TRILIUM_DATA_DIR, + "DOCUMENT_PATH": + process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"), + "BACKUP_DIR": + process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"), + "LOG_DIR": + process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"), + "ANONYMIZED_DB_DIR": + process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"), + "CONFIG_INI_PATH": + process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini") + } as const + + Object.freeze(dataDirs); + return dataDirs; +} + export function getPlatformAppDataDir(platform: ReturnType, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) { switch(true) { @@ -40,52 +86,7 @@ function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOL } } -export function getTriliumDataDir(dataDirName: string) { - // case A - if (process.env.TRILIUM_DATA_DIR) { - createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); - return process.env.TRILIUM_DATA_DIR; - } - // case B - const homePath = pathJoin(os.homedir(), dataDirName); - if (fs.existsSync(homePath)) { - return homePath; - } - - // case C - const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); - if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { - const appDataDirPath = pathJoin(platformAppDataDir, dataDirName); - createDirIfNotExisting(appDataDirPath); - return appDataDirPath; - } - - // case D - createDirIfNotExisting(homePath); - return homePath; -} - -export function getDataDirs(TRILIUM_DATA_DIR: string) { - const dataDirs = { - "TRILIUM_DATA_DIR": - TRILIUM_DATA_DIR, - "DOCUMENT_PATH": - process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"), - "BACKUP_DIR": - process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"), - "LOG_DIR": - process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"), - "ANONYMIZED_DB_DIR": - process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"), - "CONFIG_INI_PATH": - process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini") - } as const - - Object.freeze(dataDirs); - return dataDirs; - -} const TRILIUM_DATA_DIR = getTriliumDataDir(DIR_NAME); const dataDirs = getDataDirs(TRILIUM_DATA_DIR); From 5373ef509bdae43923df7242a72b2db785041e73 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 13 Jan 2025 08:28:12 +0100 Subject: [PATCH 023/108] chore(prettier): fix code style --- src/services/data_dir.ts | 76 +++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/src/services/data_dir.ts b/src/services/data_dir.ts index 9b8c70ec6..2e0c2f522 100644 --- a/src/services/data_dir.ts +++ b/src/services/data_dir.ts @@ -10,60 +10,53 @@ import os from "os"; import fs from "fs"; -import { join as pathJoin} from "path"; +import { join as pathJoin } from "path"; const DIR_NAME = "trilium-data"; const FOLDER_PERMISSIONS = 0o700; export function getTriliumDataDir(dataDirName: string) { - // case A - if (process.env.TRILIUM_DATA_DIR) { - createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); - return process.env.TRILIUM_DATA_DIR; - } + // case A + if (process.env.TRILIUM_DATA_DIR) { + createDirIfNotExisting(process.env.TRILIUM_DATA_DIR); + return process.env.TRILIUM_DATA_DIR; + } - // case B - const homePath = pathJoin(os.homedir(), dataDirName); - if (fs.existsSync(homePath)) { - return homePath; - } + // case B + const homePath = pathJoin(os.homedir(), dataDirName); + if (fs.existsSync(homePath)) { + return homePath; + } - // case C - const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); - if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { - const appDataDirPath = pathJoin(platformAppDataDir, dataDirName); - createDirIfNotExisting(appDataDirPath); - return appDataDirPath; - } + // case C + const platformAppDataDir = getPlatformAppDataDir(os.platform(), process.env.APPDATA); + if (platformAppDataDir && fs.existsSync(platformAppDataDir)) { + const appDataDirPath = pathJoin(platformAppDataDir, dataDirName); + createDirIfNotExisting(appDataDirPath); + return appDataDirPath; + } - // case D - createDirIfNotExisting(homePath); - return homePath; + // case D + createDirIfNotExisting(homePath); + return homePath; } export function getDataDirs(TRILIUM_DATA_DIR: string) { - const dataDirs = { - "TRILIUM_DATA_DIR": - TRILIUM_DATA_DIR, - "DOCUMENT_PATH": - process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"), - "BACKUP_DIR": - process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"), - "LOG_DIR": - process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"), - "ANONYMIZED_DB_DIR": - process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"), - "CONFIG_INI_PATH": - process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini") - } as const + const dataDirs = { + TRILIUM_DATA_DIR: TRILIUM_DATA_DIR, + DOCUMENT_PATH: process.env.TRILIUM_DOCUMENT_PATH || pathJoin(TRILIUM_DATA_DIR, "document.db"), + BACKUP_DIR: process.env.TRILIUM_BACKUP_DIR || pathJoin(TRILIUM_DATA_DIR, "backup"), + LOG_DIR: process.env.TRILIUM_LOG_DIR || pathJoin(TRILIUM_DATA_DIR, "log"), + ANONYMIZED_DB_DIR: process.env.TRILIUM_ANONYMIZED_DB_DIR || pathJoin(TRILIUM_DATA_DIR, "anonymized-db"), + CONFIG_INI_PATH: process.env.TRILIUM_CONFIG_INI_PATH || pathJoin(TRILIUM_DATA_DIR, "config.ini") + } as const; - Object.freeze(dataDirs); - return dataDirs; + Object.freeze(dataDirs); + return dataDirs; } export function getPlatformAppDataDir(platform: ReturnType, ENV_APPDATA_DIR: string | undefined = process.env.APPDATA) { - - switch(true) { + switch (true) { case platform === "win32" && !!ENV_APPDATA_DIR: return ENV_APPDATA_DIR; @@ -77,7 +70,6 @@ export function getPlatformAppDataDir(platform: ReturnType, // if OS is not recognized return null; } - } function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOLDER_PERMISSIONS) { @@ -86,9 +78,7 @@ function createDirIfNotExisting(path: fs.PathLike, permissionMode: fs.Mode = FOL } } - - const TRILIUM_DATA_DIR = getTriliumDataDir(DIR_NAME); const dataDirs = getDataDirs(TRILIUM_DATA_DIR); -export default dataDirs; \ No newline at end of file +export default dataDirs; From b30164ef66e90fc2b19d7d96e6027d803a685673 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 10:00:13 +0200 Subject: [PATCH 024/108] chore(e2e): add missing await to expect --- e2e/note_types/code.spec.ts | 2 +- e2e/note_types/mermaid.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e/note_types/code.spec.ts b/e2e/note_types/code.spec.ts index 6473b6849..f0f204fcc 100644 --- a/e2e/note_types/code.spec.ts +++ b/e2e/note_types/code.spec.ts @@ -10,7 +10,7 @@ test("Displays lint warnings for backend script", async ({ page, context }) => { const codeEditor = app.currentNoteSplit.locator(".CodeMirror"); // Expect two warning signs in the gutter. - expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2); + await expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2); // Hover over hello await codeEditor.getByText("hello").first().hover(); diff --git a/e2e/note_types/mermaid.spec.ts b/e2e/note_types/mermaid.spec.ts index 23f43399e..b06076cc3 100644 --- a/e2e/note_types/mermaid.spec.ts +++ b/e2e/note_types/mermaid.spec.ts @@ -6,9 +6,9 @@ test("displays simple map", async ({ page, context }) => { await app.goto(); await app.goToNoteInNewTab("Sample mindmap"); - expect(app.currentNoteSplit).toContainText("Hello world"); - expect(app.currentNoteSplit).toContainText("1"); - expect(app.currentNoteSplit).toContainText("1a"); + await expect(app.currentNoteSplit).toContainText("Hello world"); + await expect(app.currentNoteSplit).toContainText("1"); + await expect(app.currentNoteSplit).toContainText("1a"); }); test("displays note settings", async ({ page, context }) => { @@ -18,5 +18,5 @@ test("displays note settings", async ({ page, context }) => { await app.currentNoteSplit.getByText("Hello world").click({ force: true }); const nodeMenu = app.currentNoteSplit.locator(".node-menu"); - expect(nodeMenu).toBeVisible(); + await expect(nodeMenu).toBeVisible(); }); From b2e83caf4aee7d35b3c8512c95c5973b59f23a5e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 13 Jan 2025 08:38:47 +0100 Subject: [PATCH 025/108] chore(scripts): add `test-playwright` script to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 2f056ed91..cea4f3aac 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "build-frontend-docs": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", "webpack": "cross-env node --import ./loader-register.js node_modules/webpack/bin/webpack.js -c webpack.config.ts", + "test-playwright": "playwright test", "test-jasmine": "cross-env TRILIUM_DATA_DIR=./data-test tsx ./node_modules/jasmine/bin/jasmine.js", "test-es6": "tsx -r esm spec-es6/attribute_parser.spec.ts", "test": "npm run test-jasmine && npm run test-es6", From 06ebcc210e47a5e97386d7af9d1253889f31406d Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 00:54:10 +0100 Subject: [PATCH 026/108] refactor(backend_log): use async readFile using synchronous functions on the backend is not recommended, as it is "blocking the event loop", i.e. no other tasks get executed/processed, while the file is being read --- src/routes/api/backend_log.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index f57886242..ae2fec492 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -1,15 +1,15 @@ "use strict"; -import fs from "fs"; +import { readFile } from "fs/promises"; import dateUtils from "../../services/date_utils.js"; import dataDir from "../../services/data_dir.js"; const { LOG_DIR } = dataDir; -function getBackendLog() { +async function getBackendLog() { const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`; try { - return fs.readFileSync(file, "utf8"); + return await readFile(file, "utf8"); } catch (e) { // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 return ""; From eb4b5a44df780ec6be96a6b888266a9978aa55a0 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 01:06:13 +0100 Subject: [PATCH 027/108] refactor(backend_log): use path.join for log file path --- src/routes/api/backend_log.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index ae2fec492..a3ad441b4 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -1,14 +1,14 @@ "use strict"; import { readFile } from "fs/promises"; +import { join } from "path"; import dateUtils from "../../services/date_utils.js"; import dataDir from "../../services/data_dir.js"; const { LOG_DIR } = dataDir; async function getBackendLog() { - const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`; - try { + const file = join(LOG_DIR, `trilium-${dateUtils.localNowDate()}.log`); return await readFile(file, "utf8"); } catch (e) { // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 From c4ad84ab061d4277528e2b00244f4f619f4a7a09 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 01:14:16 +0100 Subject: [PATCH 028/108] refactor(backend_log): print error to the log --- src/routes/api/backend_log.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index a3ad441b4..318f894c6 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -4,6 +4,8 @@ import { readFile } from "fs/promises"; import { join } from "path"; import dateUtils from "../../services/date_utils.js"; import dataDir from "../../services/data_dir.js"; +import log from "../../services/log.js"; + const { LOG_DIR } = dataDir; async function getBackendLog() { @@ -11,6 +13,8 @@ async function getBackendLog() { const file = join(LOG_DIR, `trilium-${dateUtils.localNowDate()}.log`); return await readFile(file, "utf8"); } catch (e) { + log.error((e instanceof Error) ? e : "Reading the Backend Log failed with an unknown error."); + // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 return ""; } From 67d858441ae0ad1101cae557b8792ede69c62443 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 01:45:53 +0100 Subject: [PATCH 029/108] refactor(backend_log): include filename in log --- src/routes/api/backend_log.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index 318f894c6..acb6d3271 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -9,11 +9,12 @@ import log from "../../services/log.js"; const { LOG_DIR } = dataDir; async function getBackendLog() { + const fileName = `trilium-${dateUtils.localNowDate()}.log` try { - const file = join(LOG_DIR, `trilium-${dateUtils.localNowDate()}.log`); + const file = join(LOG_DIR, fileName); return await readFile(file, "utf8"); } catch (e) { - log.error((e instanceof Error) ? e : "Reading the Backend Log failed with an unknown error."); + log.error((e instanceof Error) ? e : `Reading the backend log '${fileName}' failed with an unknown error.`); // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 return ""; From dcfdb67539bcf67d817fa2a75d1099e148fee26e Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 13:30:39 +0100 Subject: [PATCH 030/108] refactor(backend_log): improve handle 'file not found' handle errors more "user friendly" and actually let the user know, that either the file is not existing (yet), or that reading the log failed. --- src/routes/api/backend_log.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index acb6d3271..a20e14ebd 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -14,10 +14,16 @@ async function getBackendLog() { const file = join(LOG_DIR, fileName); return await readFile(file, "utf8"); } catch (e) { - log.error((e instanceof Error) ? e : `Reading the backend log '${fileName}' failed with an unknown error.`); + const isErrorInstance = e instanceof Error; // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 - return ""; + if (isErrorInstance && "code" in e && e.code === "ENOENT") { + log.error(e); + return `The backend log file '${fileName}' does not exist (yet).`; + } + + log.error(isErrorInstance ? e : `Reading the backend log '${fileName}' failed with an unknown error: '${e}'.`); + return `Reading the backend log '${fileName}' failed.`; } } From 903988fec5e0d5cb1457688f1b8a03bfbef5efd2 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 13:41:32 +0100 Subject: [PATCH 031/108] i18n(backend_log): translate messages --- src/routes/api/backend_log.ts | 5 +++-- translations/de/server.json | 4 ++++ translations/en/server.json | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index a20e14ebd..4c45877fe 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -5,6 +5,7 @@ import { join } from "path"; import dateUtils from "../../services/date_utils.js"; import dataDir from "../../services/data_dir.js"; import log from "../../services/log.js"; +import { t } from "i18next"; const { LOG_DIR } = dataDir; @@ -19,11 +20,11 @@ async function getBackendLog() { // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 if (isErrorInstance && "code" in e && e.code === "ENOENT") { log.error(e); - return `The backend log file '${fileName}' does not exist (yet).`; + return t("backend_log.log-does-not-exist", { fileName }); } log.error(isErrorInstance ? e : `Reading the backend log '${fileName}' failed with an unknown error: '${e}'.`); - return `Reading the backend log '${fileName}' failed.`; + return t("backend_log.reading-log-failed", { fileName }); } } diff --git a/translations/de/server.json b/translations/de/server.json index a782e3c70..41bf37a70 100644 --- a/translations/de/server.json +++ b/translations/de/server.json @@ -193,5 +193,9 @@ "test_sync": { "not-configured": "Der Synchronisations-Server-Host ist nicht konfiguriert. Bitte konfiguriere zuerst die Synchronisation.", "successful": "Die Server-Verbindung wurde erfolgreich hergestellt, die Synchronisation wurde gestartet." + }, + "backend_log": { + "log-does-not-exist": "Die Backend-Log-Datei '{{ fileName }}' existiert (noch) nicht.", + "reading-log-failed": "Das Lesen der Backend-Log-Datei '{{ fileName }}' ist fehlgeschlagen." } } diff --git a/translations/en/server.json b/translations/en/server.json index fd4aa19c8..2b1d4fb6b 100644 --- a/translations/en/server.json +++ b/translations/en/server.json @@ -246,5 +246,9 @@ "new-note": "New note", "duplicate-note-suffix": "(dup)", "duplicate-note-title": "{{ noteTitle }} {{ duplicateNoteSuffix }}" + }, + "backend_log": { + "log-does-not-exist": "The backend log file '{{ fileName }}' does not exist (yet).", + "reading-log-failed": "Reading the backend log file '{{ fileName }}' failed." } } From bcbf4f40900814f929da140507c510b425d42015 Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Sat, 11 Jan 2025 14:04:44 +0100 Subject: [PATCH 032/108] chore: fix formatting --- src/routes/api/backend_log.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api/backend_log.ts b/src/routes/api/backend_log.ts index 4c45877fe..2f056158d 100644 --- a/src/routes/api/backend_log.ts +++ b/src/routes/api/backend_log.ts @@ -1,6 +1,6 @@ "use strict"; -import { readFile } from "fs/promises"; +import { readFile } from "fs/promises"; import { join } from "path"; import dateUtils from "../../services/date_utils.js"; import dataDir from "../../services/data_dir.js"; @@ -10,7 +10,7 @@ import { t } from "i18next"; const { LOG_DIR } = dataDir; async function getBackendLog() { - const fileName = `trilium-${dateUtils.localNowDate()}.log` + const fileName = `trilium-${dateUtils.localNowDate()}.log`; try { const file = join(LOG_DIR, fileName); return await readFile(file, "utf8"); From 89d700d5edec51b450693adbf4ff238ce46069b0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 17:21:50 +0200 Subject: [PATCH 033/108] chore(e2e): use different mechanism for closing all tabs --- e2e/support/app.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/e2e/support/app.ts b/e2e/support/app.ts index 08d1ebb77..d0d443b27 100644 --- a/e2e/support/app.ts +++ b/e2e/support/app.ts @@ -60,19 +60,36 @@ export default class App { return this.tabBar.locator(".note-tab[active]"); } + /** + * Closes all the tabs in the client by issuing a command. + */ async closeAllTabs() { - await this.getTab(0).click({ button: "right" }); - await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise - await this.page.getByText("Close all tabs").click({ force: true }); - await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise + await this.triggerCommand("closeAllTabs"); } + /** + * Adds a new tab by cliking on the + button near the tab bar. + */ async addNewTab() { await this.page.locator('[data-trigger-command="openNewTab"]').click(); } + /** + * Looks for a given title in the note tree and clicks on it. Useful for selecting option pages in settings in a similar fashion as the user. + * @param title the title of the note to click, as displayed in the note tree. + */ async clickNoteOnNoteTreeByTitle(title: string) { - this.noteTree.getByText(title).click(); + await this.noteTree.getByText(title).click(); + } + + /** + * Executes any Trilium command on the client. + * @param command the command to send. + */ + async triggerCommand(command: string) { + await this.page.evaluate(async (command: string) => { + await (window as any).glob.appContext.triggerCommand(command); + }, command); } } From b69cad229848502f1f5bb5191e43b83a20d1c812 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 17:42:21 +0200 Subject: [PATCH 034/108] fix(e2e): leaks if language fails --- e2e/i18n.spec.ts | 6 ++++++ e2e/support/app.ts | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/e2e/i18n.spec.ts b/e2e/i18n.spec.ts index da5be4e19..d075a61ba 100644 --- a/e2e/i18n.spec.ts +++ b/e2e/i18n.spec.ts @@ -1,6 +1,12 @@ import { test, expect, Page } from "@playwright/test"; import App from "./support/app"; +test.afterEach(async ({ page, context }) => { + const app = new App(page, context); + // Ensure English is set after each locale change to avoid any leaks to other tests. + await app.setOption("locale", "en"); +}); + test("Displays translation on desktop", async ({ page, context }) => { const app = new App(page, context); await app.goto(); diff --git a/e2e/support/app.ts b/e2e/support/app.ts index d0d443b27..290d9c420 100644 --- a/e2e/support/app.ts +++ b/e2e/support/app.ts @@ -5,6 +5,8 @@ interface GotoOpts { isMobile?: boolean; } +const BASE_URL = "http://127.0.0.1:8082"; + export default class App { readonly page: Page; readonly context: BrowserContext; @@ -27,7 +29,7 @@ export default class App { async goto(opts: GotoOpts = {}) { await this.context.addCookies([ { - url: "http://127.0.0.1:8082", + url: BASE_URL, name: "trilium-device", value: opts.isMobile ? "mobile" : "desktop" } @@ -92,4 +94,17 @@ export default class App { }, command); } + async setOption(key: string, value: string) { + const csrfToken = await this.page.evaluate(() => { + return (window as any).glob.csrfToken; + }); + + expect(csrfToken).toBeTruthy(); + await expect(await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, { + headers: { + "x-csrf-token": csrfToken + } + })).toBeOK(); + } + } From 093f9d60f038b9dd830dca74b5f2f60523a54c8d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 17:46:57 +0200 Subject: [PATCH 035/108] fix(e2e): flaky test due to timeout --- e2e/i18n.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/i18n.spec.ts b/e2e/i18n.spec.ts index d075a61ba..70713f097 100644 --- a/e2e/i18n.spec.ts +++ b/e2e/i18n.spec.ts @@ -49,9 +49,9 @@ test("User can change language from settings", async ({ page, context }) => { // Select Chinese and ensure the translation is set. await languageCombobox.selectOption("cn"); - await expect(app.currentNoteSplit).toContainText("主题"); + await expect(app.currentNoteSplit).toContainText("主题", { timeout: 15000 }); // Select English again. await languageCombobox.selectOption("en"); - await expect(app.currentNoteSplit).toContainText("Language"); + await expect(app.currentNoteSplit).toContainText("Language", { timeout: 15000 }); }); From 6c886fe3b918db6649f7ea91ffcd16fcc76f0b08 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 21:21:18 +0200 Subject: [PATCH 036/108] chore(e2e): order around docker test --- .github/workflows/main-docker.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index d7370b222..06a1d1d4c 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -48,7 +48,11 @@ jobs: node-version: 20 cache: "npm" - - run: npm ci + - name: Install npm dependencies + run: npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps - name: Run the TypeScript build run: npx tsc @@ -78,9 +82,7 @@ jobs: wait-time: 50 require-status: running require-healthy: true - - - name: Install Playwright Browsers - run: npx playwright install --with-deps + - name: Run Playwright tests run: TRILIUM_DOCKER=1 npx playwright test - uses: actions/upload-artifact@v4 From 2fa5955bd57f3602cbe2e5082a298260f914dede Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 21:41:32 +0200 Subject: [PATCH 037/108] fix(e2e): port for docker --- .github/workflows/main-docker.yml | 2 +- playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index 06a1d1d4c..eb1d5fcfb 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -72,7 +72,7 @@ jobs: - name: Validate container run output run: | - CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) + CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) echo "Container ID: $CONTAINER_ID" - name: Wait for the healthchecks to pass diff --git a/playwright.config.ts b/playwright.config.ts index 6feb9f8ab..86bf074ce 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: (!process.env.TRILIUM_DOCKER ? SERVER_URL : "http://127.0.0.1:8080"), + baseURL: SERVER_URL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', From 8b91c528aac9d7a8152576b6b0bf5897f0e7cccb Mon Sep 17 00:00:00 2001 From: Panagiotis Papadopoulos Date: Mon, 13 Jan 2025 20:49:53 +0100 Subject: [PATCH 038/108] fix(views): replace deprecated meta tag `apple-mobile-web-app-capable` => `mobile-web-app-capable` as warned by Chrome and also already implemented by e.g. Flutter or vercel/Next.js: https://github.com/vercel/next.js/pull/70363 https://github.com/flutter/flutter/issues/154596 --- src/views/desktop.ejs | 2 +- src/views/mobile.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index f56f68eb6..aa325834d 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -3,7 +3,7 @@ - + diff --git a/src/views/mobile.ejs b/src/views/mobile.ejs index 7cb664c45..f40211efe 100644 --- a/src/views/mobile.ejs +++ b/src/views/mobile.ejs @@ -3,7 +3,7 @@ - + From 1807b2b031870b0ef818b99beb14f59c604d694b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 13 Jan 2025 23:18:10 +0200 Subject: [PATCH 039/108] chore(types): missing import type for JS imports --- spec/search/becca_mocking.ts | 2 +- spec/search/parser.spec.ts | 2 +- spec/support/etapi.ts | 2 +- src/becca/becca-interface.ts | 12 ++++++------ src/becca/becca_loader.ts | 2 +- src/becca/entities/abstract_becca_entity.ts | 2 +- src/becca/entities/battachment.ts | 4 ++-- src/becca/entities/bnote.ts | 2 +- src/becca/entity_constructor.ts | 2 +- src/becca/similarity.ts | 2 +- src/etapi/app_info.ts | 2 +- src/etapi/attachments.ts | 2 +- src/etapi/attributes.ts | 2 +- src/etapi/backup.ts | 2 +- src/etapi/branches.ts | 2 +- src/etapi/mappers.ts | 8 ++++---- src/etapi/notes.ts | 2 +- src/etapi/spec.ts | 2 +- src/etapi/special_notes.ts | 2 +- src/public/app/components/app_context.ts | 8 ++++---- src/public/app/components/entrypoints.ts | 2 +- src/public/app/components/note_context.ts | 2 +- src/public/app/entities/fnote.ts | 4 ++-- src/public/app/layouts/mobile_layout.ts | 2 +- src/public/app/menus/launcher_context_menu.ts | 4 ++-- src/public/app/menus/tree_context_menu.ts | 6 +++--- src/public/app/services/attribute_renderer.ts | 4 ++-- src/public/app/services/attributes.ts | 2 +- src/public/app/services/bulk_action.ts | 2 +- src/public/app/services/froca-interface.ts | 10 +++++----- src/public/app/services/froca_updater.ts | 2 +- src/public/app/services/frontend_script_api.ts | 8 ++++---- src/public/app/services/hoisted_note.ts | 2 +- src/public/app/services/keyboard_actions.ts | 2 +- .../app/services/note_attribute_cache.ts | 2 +- src/public/app/services/note_create.ts | 4 ++-- src/public/app/services/note_list_renderer.ts | 2 +- src/public/app/services/note_tooltip.ts | 2 +- .../app/services/protected_session_holder.ts | 2 +- src/public/app/services/render.ts | 2 +- .../attribute_widgets/attribute_editor.ts | 10 +++++----- .../bulk_actions/abstract_bulk_action.ts | 2 +- src/public/app/widgets/containers/container.ts | 2 +- .../app/widgets/containers/flex_container.ts | 2 +- src/public/app/widgets/containers/launcher.ts | 2 +- .../widgets/containers/right_pane_container.ts | 2 +- src/public/app/widgets/highlights_list.ts | 2 +- .../mobile_widgets/sidebar_container.ts | 2 +- .../app/widgets/note_context_aware_widget.ts | 4 ++-- src/public/app/widgets/right_panel_widget.ts | 4 ++-- .../type_widgets/options/options_widget.ts | 2 +- src/routes/api/clipper.ts | 2 +- src/routes/api/files.ts | 4 ++-- src/routes/api/image.ts | 4 ++-- src/routes/api/import.ts | 2 +- src/routes/api/note_map.ts | 4 ++-- src/routes/api/notes.ts | 2 +- src/routes/api/revisions.ts | 6 +++--- src/routes/api/search.ts | 2 +- src/routes/api/tree.ts | 2 +- src/routes/assets.ts | 2 +- src/routes/index.ts | 2 +- src/services/attributes.ts | 2 +- src/services/backend_script_api.ts | 18 +++++++++--------- src/services/backend_script_api_interface.ts | 4 ++-- src/services/branches.ts | 2 +- src/services/bulk_actions.ts | 2 +- src/services/date_notes.ts | 2 +- src/services/export/opml.ts | 4 ++-- src/services/export/single.ts | 4 ++-- src/services/export/zip.ts | 2 +- src/services/handlers.ts | 4 ++-- src/services/import/enex.ts | 4 ++-- src/services/import/opml.ts | 4 ++-- src/services/import/single.ts | 4 ++-- src/services/import/zip.ts | 6 +++--- src/services/notes.ts | 2 +- src/services/revisions.ts | 2 +- src/services/scheduler.ts | 2 +- src/services/script.ts | 2 +- src/services/script_context.ts | 2 +- src/services/search/expressions/ancestor.ts | 2 +- src/services/search/expressions/and.ts | 4 ++-- .../search/expressions/attribute_exists.ts | 2 +- src/services/search/expressions/child_of.ts | 2 +- .../search/expressions/descendant_of.ts | 2 +- src/services/search/expressions/is_hidden.ts | 2 +- .../search/expressions/label_comparison.ts | 2 +- src/services/search/expressions/not.ts | 4 ++-- .../expressions/note_content_fulltext.ts | 2 +- .../search/expressions/note_flat_text.ts | 4 ++-- src/services/search/expressions/or.ts | 2 +- .../search/expressions/order_by_and_limit.ts | 4 ++-- src/services/search/expressions/parent_of.ts | 2 +- .../search/expressions/relation_where.ts | 2 +- src/services/search/expressions/true.ts | 4 ++-- src/services/search/note_set.ts | 2 +- src/services/search/services/parse.ts | 4 ++-- src/services/search/services/search.ts | 6 +++--- src/services/search/value_extractor.ts | 2 +- src/services/tree.ts | 2 +- src/services/ws.ts | 2 +- src/share/content_renderer.ts | 2 +- src/share/routes.ts | 6 +++--- .../shaca/entities/abstract_shaca_entity.ts | 2 +- src/share/shaca/entities/sattachment.ts | 2 +- src/share/shaca/entities/sattribute.ts | 2 +- src/share/shaca/entities/sbranch.ts | 2 +- src/share/shaca/entities/snote.ts | 6 +++--- src/share/shaca/shaca-interface.ts | 8 ++++---- 110 files changed, 178 insertions(+), 178 deletions(-) diff --git a/spec/search/becca_mocking.ts b/spec/search/becca_mocking.ts index 76ee955f6..0c65f16b6 100644 --- a/spec/search/becca_mocking.ts +++ b/spec/search/becca_mocking.ts @@ -3,7 +3,7 @@ import BBranch from "../../src/becca/entities/bbranch.js"; import BAttribute from "../../src/becca/entities/battribute.js"; import becca from "../../src/becca/becca.js"; import randtoken from "rand-token"; -import SearchResult from "../../src/services/search/search_result.js"; +import type SearchResult from "../../src/services/search/search_result.js"; import type { NoteType } from "../../src/becca/entities/rows.js"; randtoken.generator({ source: "crypto" }); diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts index 09c7ce06b..76e557067 100644 --- a/spec/search/parser.spec.ts +++ b/spec/search/parser.spec.ts @@ -1,6 +1,6 @@ import AndExp from "../../src/services/search/expressions/and.js"; import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; -import Expression from "../../src/services/search/expressions/expression.js"; +import type Expression from "../../src/services/search/expressions/expression.js"; import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; import NotExp from "../../src/services/search/expressions/not.js"; import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; diff --git a/spec/support/etapi.ts b/spec/support/etapi.ts index 3e6e1ac98..d653707d5 100644 --- a/spec/support/etapi.ts +++ b/spec/support/etapi.ts @@ -1,4 +1,4 @@ -import child_process from "child_process"; +import type child_process from "child_process"; let etapiAuthToken: string | undefined; diff --git a/src/becca/becca-interface.ts b/src/becca/becca-interface.ts index 6497dad7d..cdee5d1cd 100644 --- a/src/becca/becca-interface.ts +++ b/src/becca/becca-interface.ts @@ -1,17 +1,17 @@ import sql from "../services/sql.js"; import NoteSet from "../services/search/note_set.js"; import NotFoundError from "../errors/not_found_error.js"; -import BOption from "./entities/boption.js"; -import BNote from "./entities/bnote.js"; -import BEtapiToken from "./entities/betapi_token.js"; -import BAttribute from "./entities/battribute.js"; -import BBranch from "./entities/bbranch.js"; +import type BOption from "./entities/boption.js"; +import type BNote from "./entities/bnote.js"; +import type BEtapiToken from "./entities/betapi_token.js"; +import type BAttribute from "./entities/battribute.js"; +import type BBranch from "./entities/bbranch.js"; import BRevision from "./entities/brevision.js"; import BAttachment from "./entities/battachment.js"; import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js"; import BBlob from "./entities/bblob.js"; import BRecentNote from "./entities/brecent_note.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; interface AttachmentOpts { includeContentLength?: boolean; diff --git a/src/becca/becca_loader.ts b/src/becca/becca_loader.ts index a461828ac..8397b2dec 100644 --- a/src/becca/becca_loader.ts +++ b/src/becca/becca_loader.ts @@ -12,7 +12,7 @@ import BEtapiToken from "./entities/betapi_token.js"; import cls from "../services/cls.js"; import entityConstructor from "../becca/entity_constructor.js"; import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import ws from "../services/ws.js"; const beccaLoaded = new Promise(async (res, rej) => { diff --git a/src/becca/entities/abstract_becca_entity.ts b/src/becca/entities/abstract_becca_entity.ts index b964a2d43..a36132f3a 100644 --- a/src/becca/entities/abstract_becca_entity.ts +++ b/src/becca/entities/abstract_becca_entity.ts @@ -9,7 +9,7 @@ import cls from "../../services/cls.js"; import log from "../../services/log.js"; import protectedSessionService from "../../services/protected_session.js"; import blobService from "../../services/blob.js"; -import Becca, { type ConstructorData } from "../becca-interface.js"; +import type { default as Becca, ConstructorData } from "../becca-interface.js"; import becca from "../becca.js"; interface ContentOpts { diff --git a/src/becca/entities/battachment.ts b/src/becca/entities/battachment.ts index 1e47fc9bf..9461647af 100644 --- a/src/becca/entities/battachment.ts +++ b/src/becca/entities/battachment.ts @@ -7,8 +7,8 @@ import sql from "../../services/sql.js"; import protectedSessionService from "../../services/protected_session.js"; import log from "../../services/log.js"; import type { AttachmentRow } from "./rows.js"; -import BNote from "./bnote.js"; -import BBranch from "./bbranch.js"; +import type BNote from "./bnote.js"; +import type BBranch from "./bbranch.js"; import noteService from "../../services/notes.js"; const attachmentRoleToNoteTypeMapping = { diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index d83a263ff..acc432127 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -15,7 +15,7 @@ import dayjs from "dayjs"; import utc from "dayjs/plugin/utc.js"; import eventService from "../../services/events.js"; import type { AttachmentRow, AttributeType, NoteRow, NoteType, RevisionRow } from "./rows.js"; -import BBranch from "./bbranch.js"; +import type BBranch from "./bbranch.js"; import BAttribute from "./battribute.js"; import type { NotePojo } from "../becca-interface.js"; import searchService from "../../services/search/services/search.js"; diff --git a/src/becca/entity_constructor.ts b/src/becca/entity_constructor.ts index 83c5bd62c..18f7a14c7 100644 --- a/src/becca/entity_constructor.ts +++ b/src/becca/entity_constructor.ts @@ -1,5 +1,5 @@ import type { ConstructorData } from "./becca-interface.js"; -import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; +import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import BAttachment from "./entities/battachment.js"; import BAttribute from "./entities/battribute.js"; import BBlob from "./entities/bblob.js"; diff --git a/src/becca/similarity.ts b/src/becca/similarity.ts index 5313419d6..66af0ac61 100644 --- a/src/becca/similarity.ts +++ b/src/becca/similarity.ts @@ -3,7 +3,7 @@ import log from "../services/log.js"; import beccaService from "./becca_service.js"; import dateUtils from "../services/date_utils.js"; import { JSDOM } from "jsdom"; -import BNote from "./entities/bnote.js"; +import type BNote from "./entities/bnote.js"; const DEBUG = false; diff --git a/src/etapi/app_info.ts b/src/etapi/app_info.ts index ec5b781c2..ab6deb9dd 100644 --- a/src/etapi/app_info.ts +++ b/src/etapi/app_info.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import appInfo from "../services/app_info.js"; import eu from "./etapi_utils.js"; diff --git a/src/etapi/attachments.ts b/src/etapi/attachments.ts index 5d3ce57e4..0404e11a1 100644 --- a/src/etapi/attachments.ts +++ b/src/etapi/attachments.ts @@ -3,7 +3,7 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import v from "./validators.js"; import utils from "../services/utils.js"; -import { Router } from "express"; +import type { Router } from "express"; import type { AttachmentRow } from "../becca/entities/rows.js"; import type { ValidatorMap } from "./etapi-interface.js"; diff --git a/src/etapi/attributes.ts b/src/etapi/attributes.ts index 3b9cf3451..91a441117 100644 --- a/src/etapi/attributes.ts +++ b/src/etapi/attributes.ts @@ -3,7 +3,7 @@ import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; import attributeService from "../services/attributes.js"; import v from "./validators.js"; -import { Router } from "express"; +import type { Router } from "express"; import type { AttributeRow } from "../becca/entities/rows.js"; import type { ValidatorMap } from "./etapi-interface.js"; diff --git a/src/etapi/backup.ts b/src/etapi/backup.ts index 3b9f3f874..d73bf1a33 100644 --- a/src/etapi/backup.ts +++ b/src/etapi/backup.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import eu from "./etapi_utils.js"; import backupService from "../services/backup.js"; diff --git a/src/etapi/branches.ts b/src/etapi/branches.ts index 59f87c3c8..7263b3161 100644 --- a/src/etapi/branches.ts +++ b/src/etapi/branches.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import becca from "../becca/becca.js"; import eu from "./etapi_utils.js"; diff --git a/src/etapi/mappers.ts b/src/etapi/mappers.ts index 33cd2c29f..735e767c2 100644 --- a/src/etapi/mappers.ts +++ b/src/etapi/mappers.ts @@ -1,7 +1,7 @@ -import BAttachment from "../becca/entities/battachment.js"; -import BAttribute from "../becca/entities/battribute.js"; -import BBranch from "../becca/entities/bbranch.js"; -import BNote from "../becca/entities/bnote.js"; +import type BAttachment from "../becca/entities/battachment.js"; +import type BAttribute from "../becca/entities/battribute.js"; +import type BBranch from "../becca/entities/bbranch.js"; +import type BNote from "../becca/entities/bnote.js"; function mapNoteToPojo(note: BNote) { return { diff --git a/src/etapi/notes.ts b/src/etapi/notes.ts index 05b120195..5ab1727b9 100644 --- a/src/etapi/notes.ts +++ b/src/etapi/notes.ts @@ -9,7 +9,7 @@ import searchService from "../services/search/services/search.js"; import SearchContext from "../services/search/search_context.js"; import zipExportService from "../services/export/zip.js"; import zipImportService from "../services/import/zip.js"; -import { type Request, Router } from "express"; +import type { Request, Router } from "express"; import type { ParsedQs } from "qs"; import type { NoteParams } from "../services/note-interface.js"; import type { SearchParams } from "../services/search/services/types.js"; diff --git a/src/etapi/spec.ts b/src/etapi/spec.ts index 5e27be949..925ff51a6 100644 --- a/src/etapi/spec.ts +++ b/src/etapi/spec.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import type { Router } from "express"; import fs from "fs"; import path from "path"; diff --git a/src/etapi/special_notes.ts b/src/etapi/special_notes.ts index 14626710d..23411c987 100644 --- a/src/etapi/special_notes.ts +++ b/src/etapi/special_notes.ts @@ -2,7 +2,7 @@ import specialNotesService from "../services/special_notes.js"; import dateNotesService from "../services/date_notes.js"; import eu from "./etapi_utils.js"; import mappers from "./mappers.js"; -import { Router } from "express"; +import type { Router } from "express"; const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`); const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`); diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index ea11d2ea4..15e9b8910 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -14,15 +14,15 @@ import MainTreeExecutors from "./main_tree_executors.js"; import toast from "../services/toast.js"; import ShortcutComponent from "./shortcut_component.js"; import { t, initLocale } from "../services/i18n.js"; -import NoteDetailWidget from "../widgets/note_detail.js"; +import type NoteDetailWidget from "../widgets/note_detail.js"; import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; import type { Node } from "../services/tree.js"; -import LoadResults from "../services/load_results.js"; +import type LoadResults from "../services/load_results.js"; import type { Attribute } from "../services/attribute_parser.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; -import NoteContext, { type GetTextEditorCallback } from "./note_context.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; +import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootWidget; diff --git a/src/public/app/components/entrypoints.ts b/src/public/app/components/entrypoints.ts index b62cfdeb3..5ba9e3bd8 100644 --- a/src/public/app/components/entrypoints.ts +++ b/src/public/app/components/entrypoints.ts @@ -10,7 +10,7 @@ import bundleService from "../services/bundle.js"; import froca from "../services/froca.js"; import linkService from "../services/link.js"; import { t } from "../services/i18n.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; // TODO: Move somewhere else nicer. export type SqlExecuteResults = unknown[]; diff --git a/src/public/app/components/note_context.ts b/src/public/app/components/note_context.ts index 2bd1981cc..e5b649e56 100644 --- a/src/public/app/components/note_context.ts +++ b/src/public/app/components/note_context.ts @@ -8,7 +8,7 @@ import froca from "../services/froca.js"; import hoistedNoteService from "../services/hoisted_note.js"; import options from "../services/options.js"; import type { ViewScope } from "../services/link.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; interface SetNoteOpts { triggerSwitchEvent?: unknown; diff --git a/src/public/app/entities/fnote.ts b/src/public/app/entities/fnote.ts index ed42257e1..9759f8b07 100644 --- a/src/public/app/entities/fnote.ts +++ b/src/public/app/entities/fnote.ts @@ -5,8 +5,8 @@ import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; -import FAttachment from "./fattachment.js"; -import FAttribute, { type AttributeType } from "./fattribute.js"; +import type FAttachment from "./fattachment.js"; +import type { default as FAttribute, AttributeType } from "./fattribute.js"; import utils from "../services/utils.js"; const LABEL = "label"; diff --git a/src/public/app/layouts/mobile_layout.ts b/src/public/app/layouts/mobile_layout.ts index 0f31199fd..64e698998 100644 --- a/src/public/app/layouts/mobile_layout.ts +++ b/src/public/app/layouts/mobile_layout.ts @@ -27,7 +27,7 @@ import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolb import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; import AboutDialog from "../widgets/dialogs/about.js"; import HelpDialog from "../widgets/dialogs/help.js"; -import AppContext from "../components/app_context.js"; +import type AppContext from "../components/app_context.js"; import TabRowWidget from "../widgets/tab_row.js"; import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js"; diff --git a/src/public/app/menus/launcher_context_menu.ts b/src/public/app/menus/launcher_context_menu.ts index d61e0e372..8c2b157cd 100644 --- a/src/public/app/menus/launcher_context_menu.ts +++ b/src/public/app/menus/launcher_context_menu.ts @@ -5,7 +5,7 @@ import dialogService from "../services/dialog.js"; import server from "../services/server.js"; import { t } from "../services/i18n.js"; import type { SelectMenuItemEventListener } from "../components/events.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js"; type LauncherCommandNames = FilteredCommandNames; @@ -58,7 +58,7 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener< { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } ]; - return items.filter((row) => row !== null); + return items.filter((row) => row !== null) as MenuItem[]; } async selectMenuItemHandler({ command }: MenuCommandItem) { diff --git a/src/public/app/menus/tree_context_menu.ts b/src/public/app/menus/tree_context_menu.ts index be7983faa..650432828 100644 --- a/src/public/app/menus/tree_context_menu.ts +++ b/src/public/app/menus/tree_context_menu.ts @@ -9,8 +9,8 @@ import server from "../services/server.js"; import toastService from "../services/toast.js"; import dialogService from "../services/dialog.js"; import { t } from "../services/i18n.js"; -import NoteTreeWidget from "../widgets/note_tree.js"; -import FAttachment from "../entities/fattachment.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; +import type FAttachment from "../entities/fattachment.js"; import type { SelectMenuItemEventListener } from "../components/events.js"; // TODO: Deduplicate once client/server is well split. @@ -196,7 +196,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener row !== null); + return items.filter((row) => row !== null) as MenuItem[]; } async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem) { diff --git a/src/public/app/services/attribute_renderer.ts b/src/public/app/services/attribute_renderer.ts index a686b7098..6c7b9412d 100644 --- a/src/public/app/services/attribute_renderer.ts +++ b/src/public/app/services/attribute_renderer.ts @@ -1,7 +1,7 @@ import ws from "./ws.js"; import froca from "./froca.js"; -import FAttribute from "../entities/fattribute.js"; -import FNote from "../entities/fnote.js"; +import type FAttribute from "../entities/fattribute.js"; +import type FNote from "../entities/fnote.js"; async function renderAttribute(attribute: FAttribute, renderIsInheritable: boolean) { const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : ""; diff --git a/src/public/app/services/attributes.ts b/src/public/app/services/attributes.ts index b594630c5..767d8c3af 100644 --- a/src/public/app/services/attributes.ts +++ b/src/public/app/services/attributes.ts @@ -1,6 +1,6 @@ import server from "./server.js"; import froca from "./froca.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import type { AttributeRow } from "./load_results.js"; async function addLabel(noteId: string, name: string, value: string = "") { diff --git a/src/public/app/services/bulk_action.ts b/src/public/app/services/bulk_action.ts index 615e1c6eb..66922ef62 100644 --- a/src/public/app/services/bulk_action.ts +++ b/src/public/app/services/bulk_action.ts @@ -14,7 +14,7 @@ import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js"; import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import { t } from "./i18n.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const ACTION_GROUPS = [ { diff --git a/src/public/app/services/froca-interface.ts b/src/public/app/services/froca-interface.ts index c678e220b..8d0077989 100644 --- a/src/public/app/services/froca-interface.ts +++ b/src/public/app/services/froca-interface.ts @@ -1,8 +1,8 @@ -import FAttachment from "../entities/fattachment.js"; -import FAttribute from "../entities/fattribute.js"; -import FBlob from "../entities/fblob.js"; -import FBranch from "../entities/fbranch.js"; -import FNote from "../entities/fnote.js"; +import type FAttachment from "../entities/fattachment.js"; +import type FAttribute from "../entities/fattribute.js"; +import type FBlob from "../entities/fblob.js"; +import type FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; export interface Froca { notes: Record; diff --git a/src/public/app/services/froca_updater.ts b/src/public/app/services/froca_updater.ts index 89de8c5b7..f10720de4 100644 --- a/src/public/app/services/froca_updater.ts +++ b/src/public/app/services/froca_updater.ts @@ -6,7 +6,7 @@ import noteAttributeCache from "./note_attribute_cache.js"; import FBranch, { type FBranchRow } from "../entities/fbranch.js"; import FAttribute, { type FAttributeRow } from "../entities/fattribute.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; -import FNote, { type FNoteRow } from "../entities/fnote.js"; +import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { EntityChange } from "../server_types.js"; async function processEntityChanges(entityChanges: EntityChange[]) { diff --git a/src/public/app/services/frontend_script_api.ts b/src/public/app/services/frontend_script_api.ts index acdff7444..a8160bbff 100644 --- a/src/public/app/services/frontend_script_api.ts +++ b/src/public/app/services/frontend_script_api.ts @@ -15,11 +15,11 @@ import BasicWidget from "../widgets/basic_widget.js"; import SpacedUpdate from "./spaced_update.js"; import shortcutService from "./shortcuts.js"; import dialogService from "./dialog.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import { t } from "./i18n.js"; -import NoteContext from "../components/note_context.js"; -import NoteDetailWidget from "../widgets/note_detail.js"; -import Component from "../components/component.js"; +import type NoteContext from "../components/note_context.js"; +import type NoteDetailWidget from "../widgets/note_detail.js"; +import type Component from "../components/component.js"; /** * A whole number diff --git a/src/public/app/services/hoisted_note.ts b/src/public/app/services/hoisted_note.ts index 53d62c081..597de9467 100644 --- a/src/public/app/services/hoisted_note.ts +++ b/src/public/app/services/hoisted_note.ts @@ -2,7 +2,7 @@ import appContext from "../components/app_context.js"; import treeService, { type Node } from "./tree.js"; import dialogService from "./dialog.js"; import froca from "./froca.js"; -import NoteContext from "../components/note_context.js"; +import type NoteContext from "../components/note_context.js"; import { t } from "./i18n.js"; function getHoistedNoteId() { diff --git a/src/public/app/services/keyboard_actions.ts b/src/public/app/services/keyboard_actions.ts index bdd13e28b..dfa888620 100644 --- a/src/public/app/services/keyboard_actions.ts +++ b/src/public/app/services/keyboard_actions.ts @@ -1,7 +1,7 @@ import server from "./server.js"; import appContext, { type CommandNames } from "../components/app_context.js"; import shortcutService from "./shortcuts.js"; -import Component from "../components/component.js"; +import type Component from "../components/component.js"; const keyboardActionRepo: Record = {}; diff --git a/src/public/app/services/note_attribute_cache.ts b/src/public/app/services/note_attribute_cache.ts index ae3993cfb..18980036f 100644 --- a/src/public/app/services/note_attribute_cache.ts +++ b/src/public/app/services/note_attribute_cache.ts @@ -1,4 +1,4 @@ -import FAttribute from "../entities/fattribute.js"; +import type FAttribute from "../entities/fattribute.js"; /** * The purpose of this class is to cache the list of attributes for notes. diff --git a/src/public/app/services/note_create.ts b/src/public/app/services/note_create.ts index a7fd5ec76..b85c8277b 100644 --- a/src/public/app/services/note_create.ts +++ b/src/public/app/services/note_create.ts @@ -6,8 +6,8 @@ import froca from "./froca.js"; import treeService from "./tree.js"; import toastService from "./toast.js"; import { t } from "./i18n.js"; -import FNote from "../entities/fnote.js"; -import FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; +import type FBranch from "../entities/fbranch.js"; import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; interface CreateNoteOpts { diff --git a/src/public/app/services/note_list_renderer.ts b/src/public/app/services/note_list_renderer.ts index e6e305689..2e6652b0f 100644 --- a/src/public/app/services/note_list_renderer.ts +++ b/src/public/app/services/note_list_renderer.ts @@ -5,7 +5,7 @@ import attributeRenderer from "./attribute_renderer.js"; import libraryLoader from "./library_loader.js"; import treeService from "./tree.js"; import utils from "./utils.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const TPL = `
diff --git a/src/public/app/services/note_tooltip.ts b/src/public/app/services/note_tooltip.ts index f456d7a63..32f79c73d 100644 --- a/src/public/app/services/note_tooltip.ts +++ b/src/public/app/services/note_tooltip.ts @@ -5,7 +5,7 @@ import utils from "./utils.js"; import attributeRenderer from "./attribute_renderer.js"; import contentRenderer from "./content_renderer.js"; import appContext from "../components/app_context.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import { t } from "./i18n.js"; function setupGlobalTooltip() { diff --git a/src/public/app/services/protected_session_holder.ts b/src/public/app/services/protected_session_holder.ts index e4171f389..647c2f51d 100644 --- a/src/public/app/services/protected_session_holder.ts +++ b/src/public/app/services/protected_session_holder.ts @@ -1,4 +1,4 @@ -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; import server from "./server.js"; function enableProtectedSession() { diff --git a/src/public/app/services/render.ts b/src/public/app/services/render.ts index 0eb02c7a7..bec5cb514 100644 --- a/src/public/app/services/render.ts +++ b/src/public/app/services/render.ts @@ -1,6 +1,6 @@ import server from "./server.js"; import bundleService, { type Bundle } from "./bundle.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; async function render(note: FNote, $el: JQuery) { const relations = note.getRelations("renderNote"); diff --git a/src/public/app/widgets/attribute_widgets/attribute_editor.ts b/src/public/app/widgets/attribute_widgets/attribute_editor.ts index 0119bcd3b..622da6ca9 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_editor.ts +++ b/src/public/app/widgets/attribute_widgets/attribute_editor.ts @@ -3,17 +3,17 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; import server from "../../services/server.js"; import contextMenuService from "../../menus/context_menu.js"; -import attributeParser from "../../services/attribute_parser.js"; +import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; import libraryLoader from "../../services/library_loader.js"; import froca from "../../services/froca.js"; import attributeRenderer from "../../services/attribute_renderer.js"; import noteCreateService from "../../services/note_create.js"; import attributeService from "../../services/attributes.js"; import linkService from "../../services/link.js"; -import AttributeDetailWidget from "./attribute_detail.js"; +import type AttributeDetailWidget from "./attribute_detail.js"; import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js"; -import FAttribute, { type AttributeType } from "../../entities/fattribute.js"; -import FNote from "../../entities/fnote.js"; +import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js"; +import type FNote from "../../entities/fnote.js"; const HELP_TEXT = `

${t("attribute_editor.help_text_body1")}

@@ -417,7 +417,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem return null; } - let matchedAttr = null; + let matchedAttr: Attribute | null = null; for (const attr of parsedAttrs) { if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { diff --git a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts index 436f8fbd8..eb24bbd4d 100644 --- a/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts +++ b/src/public/app/widgets/bulk_actions/abstract_bulk_action.ts @@ -2,7 +2,7 @@ import { t } from "../../services/i18n.js"; import server from "../../services/server.js"; import ws from "../../services/ws.js"; import utils from "../../services/utils.js"; -import FAttribute from "../../entities/fattribute.js"; +import type FAttribute from "../../entities/fattribute.js"; interface ActionDefinition { script: string; diff --git a/src/public/app/widgets/containers/container.ts b/src/public/app/widgets/containers/container.ts index 56fe050a0..679fa11a8 100644 --- a/src/public/app/widgets/containers/container.ts +++ b/src/public/app/widgets/containers/container.ts @@ -1,4 +1,4 @@ -import Component, { TypedComponent } from "../../components/component.js"; +import type { default as Component, TypedComponent } from "../../components/component.js"; import BasicWidget, { TypedBasicWidget } from "../basic_widget.js"; export default class Container> extends TypedBasicWidget { diff --git a/src/public/app/widgets/containers/flex_container.ts b/src/public/app/widgets/containers/flex_container.ts index 62660a4ee..274284775 100644 --- a/src/public/app/widgets/containers/flex_container.ts +++ b/src/public/app/widgets/containers/flex_container.ts @@ -1,4 +1,4 @@ -import { TypedComponent } from "../../components/component.js"; +import type { TypedComponent } from "../../components/component.js"; import Container from "./container.js"; export type FlexDirection = "row" | "column"; diff --git a/src/public/app/widgets/containers/launcher.ts b/src/public/app/widgets/containers/launcher.ts index 41024073a..86fbabb96 100644 --- a/src/public/app/widgets/containers/launcher.ts +++ b/src/public/app/widgets/containers/launcher.ts @@ -11,7 +11,7 @@ import utils from "../../services/utils.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js"; import HistoryNavigationButton from "../buttons/history_navigation.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js"; -import FNote from "../../entities/fnote.js"; +import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; interface InnerWidget extends BasicWidget { diff --git a/src/public/app/widgets/containers/right_pane_container.ts b/src/public/app/widgets/containers/right_pane_container.ts index 4325935fe..c7632769b 100644 --- a/src/public/app/widgets/containers/right_pane_container.ts +++ b/src/public/app/widgets/containers/right_pane_container.ts @@ -1,6 +1,6 @@ import FlexContainer from "./flex_container.js"; import splitService from "../../services/resizer.js"; -import RightPanelWidget from "../right_panel_widget.js"; +import type RightPanelWidget from "../right_panel_widget.js"; export default class RightPaneContainer extends FlexContainer { private rightPaneHidden: boolean; diff --git a/src/public/app/widgets/highlights_list.ts b/src/public/app/widgets/highlights_list.ts index 8b8fb06de..a9be2ddfa 100644 --- a/src/public/app/widgets/highlights_list.ts +++ b/src/public/app/widgets/highlights_list.ts @@ -12,7 +12,7 @@ import options from "../services/options.js"; import OnClickButtonWidget from "./buttons/onclick_button.js"; import appContext, { type EventData } from "../components/app_context.js"; import libraryLoader from "../services/library_loader.js"; -import FNote from "../entities/fnote.js"; +import type FNote from "../entities/fnote.js"; const TPL = `