mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-09 16:00:15 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 528b5b5394 | |||
| 67da3964e5 | |||
| 45ebc7eb6b | |||
| cc5ead68bc | |||
| 9b9e4f22f3 | |||
| 7823f46053 | |||
| d6d8e548b3 | |||
| 44b1ed7610 | |||
| 5786da5534 | |||
| 042a80dd44 | |||
| ee30f6cd6d | |||
| 977a300808 | |||
| a392ccab6a | |||
| dfdfa5383b | |||
| a08294ab16 | |||
| 502ebafdda | |||
| bba0a27735 | |||
| 02580b2cad | |||
| 13bb44c9bb | |||
| eeb508ed32 | |||
| edf85184c1 |
@@ -53,11 +53,12 @@ The hatch report (below) includes version information. If you cannot provide the
|
|||||||
|
|
||||||
- Self-hosted LiveSync version: <!-- e.g. 0.23.0 — find it in Obsidian Settings → Community Plugins -->
|
- Self-hosted LiveSync version: <!-- e.g. 0.23.0 — find it in Obsidian Settings → Community Plugins -->
|
||||||
|
|
||||||
### Report from LiveSync
|
### Report and Logs from LiveSync
|
||||||
Open the `Hatch` pane in LiveSync settings and press `Make report`. Paste here or upload to [Gist](https://gist.github.com/) and share the link.
|
Perform a `Generate full report for opening the issue with debug info` command and provide the generated report. This contains detailed information and recent 1000 log lines, which is very helpful for debugging. **PLEASE AMEND THE REPORT TO REMOVE ANY SENSITIVE INFORMATION BEFORE PASTING.**
|
||||||
|
If too large to paste here, upload to [Gist](https://gist.github.com/) and share the link.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Report from hatch (primary)</summary>
|
<summary>Report and Logs (primary)</summary>
|
||||||
|
|
||||||
```
|
```
|
||||||
<!-- paste here or link to Gist -->
|
<!-- paste here or link to Gist -->
|
||||||
@@ -65,29 +66,7 @@ Open the `Hatch` pane in LiveSync settings and press `Make report`. Paste here o
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Report from hatch (if applicable)</summary>
|
<summary>Report and Logs (if applicable)</summary>
|
||||||
|
|
||||||
```
|
|
||||||
<!-- paste here or link to Gist -->
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
### Plug-in log
|
|
||||||
Enable `Verbose Log` in General Settings first, then reproduce the issue and copy the log (tap the document box icon in the ribbon).
|
|
||||||
Paste here or upload to [Gist](https://gist.github.com/) and share the link.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Plug-in log (primary)</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
<!-- paste here or link to Gist -->
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Plug-in log (if applicable)</summary>
|
|
||||||
|
|
||||||
```
|
```
|
||||||
<!-- paste here or link to Gist -->
|
<!-- paste here or link to Gist -->
|
||||||
|
|||||||
@@ -29,3 +29,9 @@ cov_profile/**
|
|||||||
|
|
||||||
coverage
|
coverage
|
||||||
src/apps/cli/dist/*
|
src/apps/cli/dist/*
|
||||||
|
|
||||||
|
# Obsidian E2E test artefacts
|
||||||
|
test_e2e/playwright-report/
|
||||||
|
test_e2e/test-results/
|
||||||
|
_testdata/**
|
||||||
|
utils/bench/splitResults.csv
|
||||||
|
|||||||
@@ -255,14 +255,20 @@ It depends on Obsidian detects. May toggling `Detect all extensions` of
|
|||||||
|
|
||||||
### I hope to report the issue, but you said you needs `Report`. How to make it?
|
### I hope to report the issue, but you said you needs `Report`. How to make it?
|
||||||
|
|
||||||
We can copy the report to the clipboard, by pressing the `Make report` button on
|
We can copy the report to the clipboard, by performing
|
||||||
the `Hatch` pane. 
|
`Generate full report for opening the issue with debug info` command!
|
||||||
|
|
||||||
### Where can I check the log?
|
### Where can I check the log?
|
||||||
|
|
||||||
We can launch the log pane by `Show log` on the command palette. And if you have
|
We can launch the log pane by `Show log` on the command palette. And if you have
|
||||||
troubled something, please enable the `Verbose Log` on the `General Setting`
|
troubled something, please enable the `Verbose Log` on the `General Setting`
|
||||||
pane.
|
pane.
|
||||||
|
`Generate full report for opening the issue with debug info` command also contains
|
||||||
|
the recent 1000 log lines, which is very helpful for debugging. Full-report is
|
||||||
|
already set to the verbose level, so it contains all the logs without enabling the
|
||||||
|
`Verbose Log` toggle.
|
||||||
|
|
||||||
|
Let me note that please be sure to remove any sensitive information before sharing the report.
|
||||||
|
|
||||||
However, the logs would not be kept so long and cleared when restarted. If you
|
However, the logs would not be kept so long and cleared when restarted. If you
|
||||||
want to check the logs, please enable `Write logs into the file` temporarily.
|
want to check the logs, please enable `Write logs into the file` temporarily.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.64",
|
"version": "0.25.65",
|
||||||
"minAppVersion": "1.7.2",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
Generated
+107
-151
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.64",
|
"version": "0.25.65",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.64",
|
"version": "0.25.65",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.808.0",
|
"@aws-sdk/client-s3": "^3.808.0",
|
||||||
@@ -1851,9 +1851,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1887,7 +1887,7 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers/node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||||
@@ -1932,9 +1932,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1984,19 +1984,6 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/json/node_modules/@eslint/core": {
|
|
||||||
"version": "0.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@eslint/object-schema": {
|
"node_modules/@eslint/object-schema": {
|
||||||
"version": "2.1.7",
|
"version": "2.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||||
@@ -2021,19 +2008,6 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
|
|
||||||
"version": "0.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@fidm/asn1": {
|
"node_modules/@fidm/asn1": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz",
|
||||||
@@ -3572,20 +3546,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/core": {
|
"node_modules/@smithy/core": {
|
||||||
"version": "3.23.12",
|
"version": "3.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz",
|
||||||
"integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==",
|
"integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/protocol-http": "^5.3.12",
|
"@aws-crypto/crc32": "5.2.0",
|
||||||
"@smithy/types": "^4.13.1",
|
"@smithy/types": "^4.14.2",
|
||||||
"@smithy/url-parser": "^4.2.12",
|
|
||||||
"@smithy/util-base64": "^4.3.2",
|
|
||||||
"@smithy/util-body-length-browser": "^4.2.2",
|
|
||||||
"@smithy/util-middleware": "^4.2.12",
|
|
||||||
"@smithy/util-stream": "^4.5.20",
|
|
||||||
"@smithy/util-utf8": "^4.2.2",
|
|
||||||
"@smithy/uuid": "^1.1.2",
|
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3752,11 +3719,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/is-array-buffer": {
|
"node_modules/@smithy/is-array-buffer": {
|
||||||
"version": "4.2.2",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.3.3.tgz",
|
||||||
"integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==",
|
"integrity": "sha512-RRxYqjUa/n8dRVkbhyuiRarppLzt4H/AtMUEFmiHlDy8o4wrgqAdzxsk9naemzu6iX67ZV375fNmX7Q8dynGKw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@smithy/core": "^3.24.3",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4020,9 +3988,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/types": {
|
"node_modules/@smithy/types": {
|
||||||
"version": "4.14.1",
|
"version": "4.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz",
|
||||||
"integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==",
|
"integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
@@ -4084,12 +4052,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/util-buffer-from": {
|
"node_modules/@smithy/util-buffer-from": {
|
||||||
"version": "4.2.2",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.3.3.tgz",
|
||||||
"integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==",
|
"integrity": "sha512-5xlgilVaX96HdVlLZymKUa7vOTZtisOTxBJloM2J4PeRqyAWBeFIq0DnIxQISvwxT4rgJAvk7rHhB+GlCCKe8g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/is-array-buffer": "^4.2.2",
|
"@smithy/core": "^3.24.3",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4226,12 +4194,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/util-utf8": {
|
"node_modules/@smithy/util-utf8": {
|
||||||
"version": "4.2.2",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.3.3.tgz",
|
||||||
"integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==",
|
"integrity": "sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/util-buffer-from": "^4.2.2",
|
"@smithy/core": "^3.24.3",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4521,9 +4489,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "24.12.2",
|
"version": "24.12.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
|
||||||
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5255,9 +5223,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/config/node_modules/brace-expansion": {
|
"node_modules/@wdio/config/node_modules/brace-expansion": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5377,9 +5345,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/repl/node_modules/@types/node": {
|
"node_modules/@wdio/repl/node_modules/@types/node": {
|
||||||
"version": "20.19.39",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5407,9 +5375,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@wdio/types/node_modules/@types/node": {
|
"node_modules/@wdio/types/node_modules/@types/node": {
|
||||||
"version": "20.19.39",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5532,9 +5500,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.14.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5623,9 +5591,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
"node_modules/archiver-utils/node_modules/brace-expansion": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6234,9 +6202,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.5",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
@@ -7150,9 +7118,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.4.0",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
"integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
|
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -7914,9 +7882,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-import/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -8262,14 +8230,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
"node_modules/eslint-plugin-react/node_modules/resolve": {
|
||||||
"version": "2.0.0-next.6",
|
"version": "2.0.0-next.7",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
|
||||||
"integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==",
|
"integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"is-core-module": "^2.16.1",
|
"is-core-module": "^2.16.2",
|
||||||
"node-exports-info": "^1.6.0",
|
"node-exports-info": "^1.6.0",
|
||||||
"object-keys": "^1.1.1",
|
"object-keys": "^1.1.1",
|
||||||
"path-parse": "^1.0.7",
|
"path-parse": "^1.0.7",
|
||||||
@@ -8432,19 +8400,6 @@
|
|||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/@eslint/core": {
|
|
||||||
"version": "0.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
|
||||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint/node_modules/balanced-match": {
|
"node_modules/eslint/node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -8453,9 +8408,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/brace-expansion": {
|
"node_modules/eslint/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9289,9 +9244,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/globby/node_modules/brace-expansion": {
|
"node_modules/globby/node_modules/brace-expansion": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9443,9 +9398,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9786,13 +9741,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.2"
|
"hasown": "^2.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -10973,9 +10928,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "11.2.7",
|
"version": "11.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz",
|
||||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
"integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -12695,9 +12650,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
"node_modules/readdir-glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -12817,12 +12772,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.12",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"is-core-module": "^2.16.1",
|
"is-core-module": "^2.16.1",
|
||||||
"path-parse": "^1.0.7",
|
"path-parse": "^1.0.7",
|
||||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
@@ -13119,9 +13075,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
|
||||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -15293,9 +15249,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "7.24.7",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||||
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
|
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -16157,9 +16113,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest/node_modules/es-module-lexer": {
|
"node_modules/vitest/node_modules/es-module-lexer": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
|
||||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -16240,9 +16196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriver/node_modules/@types/node": {
|
"node_modules/webdriver/node_modules/@types/node": {
|
||||||
"version": "20.19.39",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -16250,9 +16206,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriver/node_modules/undici": {
|
"node_modules/webdriver/node_modules/undici": {
|
||||||
"version": "6.24.1",
|
"version": "6.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -16312,9 +16268,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webdriverio/node_modules/@types/node": {
|
"node_modules/webdriverio/node_modules/@types/node": {
|
||||||
"version": "20.19.39",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
"integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
|
"integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -16778,9 +16734,9 @@
|
|||||||
"license": "BSD"
|
"license": "BSD"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -16848,9 +16804,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.3",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
||||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.64",
|
"version": "0.25.65",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -55,6 +55,9 @@
|
|||||||
"test:docker-all:stop": "npm run test:docker-all:down",
|
"test:docker-all:stop": "npm run test:docker-all:down",
|
||||||
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
|
"test:full": "npm run test:docker-all:start && vitest run --coverage && npm run test:docker-all:stop",
|
||||||
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
|
"test:p2p": "bash test/suitep2p/run-p2p-tests.sh",
|
||||||
|
"test:obsidian:e2e": "npx playwright test --config test_e2e/playwright.config.ts",
|
||||||
|
"test:obsidian:e2e:headed": "npx playwright test --config test_e2e/playwright.config.ts --headed",
|
||||||
|
"test:obsidian:build-and-e2e": "npm run buildDev && npm run test:obsidian:e2e",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/models/setting.const";
|
||||||
|
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
|
||||||
|
import { generateCredentialObject } from "@lib/replication/httplib";
|
||||||
|
import { parseHeaderValues } from "@lib/common/utils";
|
||||||
|
import { requestToCouchDBWithCredentials } from "./utils";
|
||||||
|
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
|
||||||
|
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
|
||||||
|
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions";
|
||||||
|
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars";
|
||||||
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
|
function redactObject(obj: Record<string, any>, dotted: string, redactedValue = "REDACTED") {
|
||||||
|
const keys = dotted.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
if (!(key in current)) {
|
||||||
|
current[key] = {} as Record<string, any>;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
const lastKey = keys[keys.length - 1];
|
||||||
|
if (lastKey in current) {
|
||||||
|
current[lastKey] = redactedValue;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
export async function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore) {
|
||||||
|
let responseConfig: Record<string, any> = {};
|
||||||
|
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
|
||||||
|
if (settings.remoteType == REMOTE_COUCHDB) {
|
||||||
|
try {
|
||||||
|
const credential = generateCredentialObject(settings);
|
||||||
|
const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
|
||||||
|
const r = await requestToCouchDBWithCredentials(
|
||||||
|
settings.couchDB_URI,
|
||||||
|
credential,
|
||||||
|
window.origin,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
customHeaders
|
||||||
|
);
|
||||||
|
responseConfig = r.json as Record<string, any>;
|
||||||
|
redactObject(responseConfig, "couch_httpd_auth.secret");
|
||||||
|
redactObject(responseConfig, "couch_httpd_auth.authentication_db");
|
||||||
|
redactObject(responseConfig, "couch_httpd_auth.authentication_redirect");
|
||||||
|
redactObject(responseConfig, "couchdb.uuid");
|
||||||
|
redactObject(responseConfig, "admins");
|
||||||
|
redactObject(responseConfig, "users");
|
||||||
|
redactObject(responseConfig, "chttpd_auth.secret");
|
||||||
|
delete responseConfig["jwt_keys"];
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
responseConfig = {
|
||||||
|
error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (settings.remoteType == REMOTE_MINIO) {
|
||||||
|
responseConfig = { error: "Object Storage Synchronisation" };
|
||||||
|
//
|
||||||
|
}
|
||||||
|
const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[];
|
||||||
|
const pluginConfig = JSON.parse(JSON.stringify(settings)) as ObsidianLiveSyncSettings;
|
||||||
|
const pluginKeys = Object.keys(pluginConfig);
|
||||||
|
for (const key of pluginKeys) {
|
||||||
|
if (defaultKeys.includes(key as keyof ObsidianLiveSyncSettings)) continue;
|
||||||
|
delete pluginConfig[key as keyof ObsidianLiveSyncSettings];
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginConfig.couchDB_DBNAME = REDACTED;
|
||||||
|
pluginConfig.couchDB_PASSWORD = REDACTED;
|
||||||
|
const scheme = pluginConfig.couchDB_URI.startsWith("http:")
|
||||||
|
? "(HTTP)"
|
||||||
|
: pluginConfig.couchDB_URI.startsWith("https:")
|
||||||
|
? "(HTTPS)"
|
||||||
|
: "";
|
||||||
|
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : `self-hosted${scheme}`;
|
||||||
|
pluginConfig.couchDB_USER = REDACTED;
|
||||||
|
pluginConfig.passphrase = REDACTED;
|
||||||
|
pluginConfig.encryptedPassphrase = REDACTED;
|
||||||
|
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
||||||
|
pluginConfig.accessKey = REDACTED;
|
||||||
|
pluginConfig.secretKey = REDACTED;
|
||||||
|
const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
|
||||||
|
const toSchemeOnly = (uri: string) => {
|
||||||
|
try {
|
||||||
|
return `${new URL(uri).protocol}//`;
|
||||||
|
} catch {
|
||||||
|
const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//);
|
||||||
|
return matched?.[0] ?? REDACTED;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pluginConfig.remoteConfigurations = Object.fromEntries(
|
||||||
|
Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
...config,
|
||||||
|
uri: toSchemeOnly(config.uri),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
);
|
||||||
|
pluginConfig.region = redact(pluginConfig.region);
|
||||||
|
pluginConfig.bucket = redact(pluginConfig.bucket);
|
||||||
|
pluginConfig.pluginSyncExtendedSetting = {};
|
||||||
|
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
|
||||||
|
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
|
||||||
|
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
|
||||||
|
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
|
||||||
|
pluginConfig.jwtKey = redact(pluginConfig.jwtKey);
|
||||||
|
pluginConfig.jwtSub = redact(pluginConfig.jwtSub);
|
||||||
|
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
|
||||||
|
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
|
||||||
|
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
|
||||||
|
pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential);
|
||||||
|
pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername);
|
||||||
|
pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`;
|
||||||
|
const endpoint = pluginConfig.endpoint;
|
||||||
|
if (endpoint == "") {
|
||||||
|
pluginConfig.endpoint = "Not configured or AWS";
|
||||||
|
} else {
|
||||||
|
const endpointScheme = pluginConfig.endpoint.startsWith("http:")
|
||||||
|
? "(HTTP)"
|
||||||
|
: pluginConfig.endpoint.startsWith("https:")
|
||||||
|
? "(HTTPS)"
|
||||||
|
: "";
|
||||||
|
pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`;
|
||||||
|
}
|
||||||
|
const obsidianInfo = {
|
||||||
|
navigator: compatGlobal.navigator.userAgent,
|
||||||
|
fileSystem: core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
|
||||||
|
};
|
||||||
|
const result = {
|
||||||
|
obsidianInfo,
|
||||||
|
responseConfig,
|
||||||
|
pluginConfig,
|
||||||
|
manifestVersion,
|
||||||
|
packageVersion,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -16,10 +16,7 @@
|
|||||||
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
|
||||||
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
import { ConnectionStringParser } from "@lib/common/ConnectionString";
|
||||||
import type { P2PSyncSetting, RemoteConfiguration } from "@lib/common/models/setting.type";
|
import type { P2PSyncSetting, RemoteConfiguration } from "@lib/common/models/setting.type";
|
||||||
import {
|
import { activateP2PRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
|
||||||
activateP2PRemoteConfiguration,
|
|
||||||
createRemoteConfigurationId,
|
|
||||||
} from "@lib/serviceFeatures/remoteConfig";
|
|
||||||
import { extractP2PRoomSuffix } from "@lib/common/utils";
|
import { extractP2PRoomSuffix } from "@lib/common/utils";
|
||||||
import { SetupManager } from "@/modules/features/SetupManager";
|
import { SetupManager } from "@/modules/features/SetupManager";
|
||||||
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
|
||||||
@@ -36,9 +33,7 @@
|
|||||||
let replicatingPeerId = $state<string | null>(null);
|
let replicatingPeerId = $state<string | null>(null);
|
||||||
let communicatingUntil = $state<Record<string, number>>({});
|
let communicatingUntil = $state<Record<string, number>>({});
|
||||||
const COMMUNICATION_HOLD_MS = 2500;
|
const COMMUNICATION_HOLD_MS = 2500;
|
||||||
let syncOnReplicationSetting = $state(
|
let syncOnReplicationSetting = $state(core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "");
|
||||||
core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? ""
|
|
||||||
);
|
|
||||||
type P2PRemoteOption = {
|
type P2PRemoteOption = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -51,12 +46,19 @@
|
|||||||
let selectingP2PRemote = $state(false);
|
let selectingP2PRemote = $state(false);
|
||||||
|
|
||||||
function addToList(item: string, list: string): string {
|
function addToList(item: string, list: string): string {
|
||||||
const items = list.split(",").map((e) => e.trim()).filter((e) => e);
|
const items = list
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter((e) => e);
|
||||||
if (!items.includes(item)) items.push(item);
|
if (!items.includes(item)) items.push(item);
|
||||||
return items.join(",");
|
return items.join(",");
|
||||||
}
|
}
|
||||||
function removeFromList(item: string, list: string): string {
|
function removeFromList(item: string, list: string): string {
|
||||||
return list.split(",").map((e) => e.trim()).filter((e) => e && e !== item).join(",");
|
return list
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter((e) => e && e !== item)
|
||||||
|
.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
function markCommunicating(peerId: string) {
|
function markCommunicating(peerId: string) {
|
||||||
@@ -409,7 +411,12 @@
|
|||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<button class="icon-button" onclick={() => createAndSelectP2PRemote()} title="Create P2P remote" aria-label="Create P2P remote">
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
onclick={() => createAndSelectP2PRemote()}
|
||||||
|
title="Create P2P remote"
|
||||||
|
aria-label="Create P2P remote"
|
||||||
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -442,7 +449,8 @@
|
|||||||
<div class="peer-item">
|
<div class="peer-item">
|
||||||
<div class="peer-info">
|
<div class="peer-info">
|
||||||
<div class="peer-name">
|
<div class="peer-name">
|
||||||
{peer.name} : <span class="peer-id-mini" title={peer.peerId}>({peer.peerId.slice(0, 8)})</span>
|
{peer.name} :
|
||||||
|
<span class="peer-id-mini" title={peer.peerId}>({peer.peerId.slice(0, 8)})</span>
|
||||||
{#if isCommunicating(peer.peerId)}
|
{#if isCommunicating(peer.peerId)}
|
||||||
<span class="comm-icon" title="Communicating" aria-label="Communicating">📡</span>
|
<span class="comm-icon" title="Communicating" aria-label="Communicating">📡</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -460,11 +468,11 @@
|
|||||||
<button
|
<button
|
||||||
class="emoji-button"
|
class="emoji-button"
|
||||||
disabled={replicatingPeerId !== null}
|
disabled={replicatingPeerId !== null}
|
||||||
title={replicatingPeerId === peer.peerId ? 'Replicating...' : 'Replicate now'}
|
title={replicatingPeerId === peer.peerId ? "Replicating..." : "Replicate now"}
|
||||||
aria-label={replicatingPeerId === peer.peerId ? 'Replicating' : 'Replicate now'}
|
aria-label={replicatingPeerId === peer.peerId ? "Replicating" : "Replicate now"}
|
||||||
onclick={() => startReplication(peer)}
|
onclick={() => startReplication(peer)}
|
||||||
>
|
>
|
||||||
{replicatingPeerId === peer.peerId ? '⏳' : '🔄'}
|
{replicatingPeerId === peer.peerId ? "⏳" : "🔄"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
@@ -478,25 +486,31 @@
|
|||||||
<span class="decision-label">WATCH</span>
|
<span class="decision-label">WATCH</span>
|
||||||
<button
|
<button
|
||||||
class="emoji-button {isWatching(peer.peerId) ? 'is-watching' : ''}"
|
class="emoji-button {isWatching(peer.peerId) ? 'is-watching' : ''}"
|
||||||
title={isWatching(peer.peerId) ? 'Watching this peer \u2014 click to stop' : 'Watch this peer\'s changes'}
|
title={isWatching(peer.peerId)
|
||||||
aria-label={isWatching(peer.peerId) ? 'Stop watching' : 'Watch peer'}
|
? "Watching this peer \u2014 click to stop"
|
||||||
|
: "Watch this peer's changes"}
|
||||||
|
aria-label={isWatching(peer.peerId) ? "Stop watching" : "Watch peer"}
|
||||||
disabled={!canEditP2PSettings()}
|
disabled={!canEditP2PSettings()}
|
||||||
onclick={() => toggleWatch(peer.peerId)}
|
onclick={() => toggleWatch(peer.peerId)}
|
||||||
>
|
>
|
||||||
{isWatching(peer.peerId) ? '🔔' : '🔕'}
|
{isWatching(peer.peerId) ? "🔔" : "🔕"}
|
||||||
</button>
|
</button>
|
||||||
</div> <div class="decision-row watch-row">
|
</div>
|
||||||
|
<div class="decision-row watch-row">
|
||||||
<span class="decision-label">SYNC</span>
|
<span class="decision-label">SYNC</span>
|
||||||
<button
|
<button
|
||||||
class="emoji-button {isSyncTarget(peer.name) ? 'is-watching' : ''}"
|
class="emoji-button {isSyncTarget(peer.name) ? 'is-watching' : ''}"
|
||||||
title={isSyncTarget(peer.name) ? 'Sync target \u2014 click to remove' : 'Set as sync target'}
|
title={isSyncTarget(peer.name)
|
||||||
aria-label={isSyncTarget(peer.name) ? 'Remove sync target' : 'Set sync target'}
|
? "Sync target \u2014 click to remove"
|
||||||
|
: "Set as sync target"}
|
||||||
|
aria-label={isSyncTarget(peer.name) ? "Remove sync target" : "Set sync target"}
|
||||||
disabled={!canEditP2PSettings()}
|
disabled={!canEditP2PSettings()}
|
||||||
onclick={() => toggleSyncTarget(peer)}
|
onclick={() => toggleSyncTarget(peer)}
|
||||||
>
|
>
|
||||||
{isSyncTarget(peer.name) ? '🔗' : '⛓️💥'}
|
{isSyncTarget(peer.name) ? "🔗" : "⛓️💥"}
|
||||||
</button>
|
</button>
|
||||||
</div> {:else}
|
</div>
|
||||||
|
{:else}
|
||||||
<div class="decision-status">
|
<div class="decision-status">
|
||||||
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
|
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
|
||||||
{getAcceptanceStatus(peer)}
|
{getAcceptanceStatus(peer)}
|
||||||
@@ -571,7 +585,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.peers-section {
|
.peers-section {
|
||||||
@@ -584,7 +597,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,8 +616,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remote-picker {
|
.remote-picker {
|
||||||
max-width: 14rem;
|
max-width: 10rem;
|
||||||
min-width: 8rem;
|
min-width: 1em;
|
||||||
|
flex-shrink: 1;
|
||||||
height: 1.9rem;
|
height: 1.9rem;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
@@ -648,6 +662,7 @@
|
|||||||
|
|
||||||
.peers-header {
|
.peers-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -873,5 +888,4 @@
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
+1
-1
Submodule src/lib updated: 36b99354f6...6abcea69eb
@@ -121,7 +121,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isHidden = document.hidden;
|
const isHidden = activeWindow.document.hidden;
|
||||||
if (this.isLastHidden === isHidden) {
|
if (this.isLastHidden === isHidden) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
|
|||||||
} else {
|
} else {
|
||||||
// suspend all temporary.
|
// suspend all temporary.
|
||||||
if (this.services.appLifecycle.isSuspended()) return;
|
if (this.services.appLifecycle.isSuspended()) return;
|
||||||
if (!this.hasFocus) return;
|
// Do not block resume by focus state here; visibility recovery should be enough.
|
||||||
await this.services.appLifecycle.onResuming();
|
await this.services.appLifecycle.onResuming();
|
||||||
await this.services.appLifecycle.onResumed();
|
await this.services.appLifecycle.onResumed();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
EVENT_ON_UNRESOLVED_ERROR,
|
EVENT_ON_UNRESOLVED_ERROR,
|
||||||
} from "../../common/events.ts";
|
} from "../../common/events.ts";
|
||||||
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
|
||||||
import { addIcon, normalizePath, Notice } from "../../deps.ts";
|
import { addIcon, debounce, normalizePath, Notice, stringifyYaml, type WorkspaceLeaf } from "../../deps.ts";
|
||||||
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
|
||||||
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
|
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
|
||||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||||
@@ -41,6 +41,8 @@ import {
|
|||||||
} from "@lib/string_and_binary/path.ts";
|
} from "@lib/string_and_binary/path.ts";
|
||||||
import { MARK_LOG_NETWORK_ERROR, MARK_LOG_SEPARATOR } from "@lib/services/lib/logUtils.ts";
|
import { MARK_LOG_NETWORK_ERROR, MARK_LOG_SEPARATOR } from "@lib/services/lib/logUtils.ts";
|
||||||
import { NetworkWarningStyles } from "@lib/common/models/setting.const.ts";
|
import { NetworkWarningStyles } from "@lib/common/models/setting.const.ts";
|
||||||
|
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||||
|
import { generateReport } from "@/common/reportTool.ts";
|
||||||
|
|
||||||
// This module cannot be a core module because it depends on the Obsidian UI.
|
// This module cannot be a core module because it depends on the Obsidian UI.
|
||||||
|
|
||||||
@@ -50,18 +52,51 @@ const globalLogFunction = (message: any, level?: number, key?: string) => {
|
|||||||
const messageX =
|
const messageX =
|
||||||
message instanceof Error
|
message instanceof Error
|
||||||
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
|
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
|
||||||
: message;
|
: typeof message === "string"
|
||||||
|
? message
|
||||||
|
: JSON.stringify(message);
|
||||||
const entry = { message: messageX, level, key } as LogEntry;
|
const entry = { message: messageX, level, key } as LogEntry;
|
||||||
recentLogEntries.value = [...recentLogEntries.value, entry];
|
recentLogEntries.value = [...recentLogEntries.value, entry];
|
||||||
};
|
};
|
||||||
|
|
||||||
setGlobalLogFunction(globalLogFunction);
|
setGlobalLogFunction(globalLogFunction);
|
||||||
let recentLogs = [] as string[];
|
// Keep the recent logs in memory for display, but also keep a longer history in logForDump for when the user wants to see more logs.
|
||||||
|
// logForDump is not reactive and is only used for dumping logs when requested, while recentLogs is reactive and is used for displaying logs in the UI.
|
||||||
|
const logForDump = [] as string[];
|
||||||
|
|
||||||
function addLog(log: string) {
|
function addLog(log: string) {
|
||||||
recentLogs = [...recentLogs, log].splice(-200);
|
logForDump.push(log);
|
||||||
logMessages.value = recentLogs;
|
while (logForDump.length > 1000) {
|
||||||
|
logForDump.shift();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display log is kept separate from the full log history to optimize performance and memory usage.
|
||||||
|
// And debounce the updates to the display log to avoid excessive UI updates when there are many log entries in a short time.
|
||||||
|
const logForDisplay = [] as string[];
|
||||||
|
|
||||||
|
const updateLogMessage = debounce(() => {
|
||||||
|
logMessages.value = [...logForDisplay];
|
||||||
|
}, 25);
|
||||||
|
function addDisplayLog(log: string) {
|
||||||
|
logForDisplay.push(log);
|
||||||
|
while (logForDisplay.length > 200) {
|
||||||
|
logForDisplay.shift();
|
||||||
|
}
|
||||||
|
updateLogMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
const redactPatterns = [/PBKDF2 salt \(Security Seed\):.*$/];
|
||||||
|
function redactLog(log: string) {
|
||||||
|
let redactedLog = log;
|
||||||
|
for (const pattern of redactPatterns) {
|
||||||
|
redactedLog = redactedLog.replace(pattern, (match) => {
|
||||||
|
return match.split(":")[0] + ": [REDACTED]";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return redactedLog;
|
||||||
|
}
|
||||||
|
|
||||||
// logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
|
// logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
|
||||||
|
|
||||||
const showDebugLog = false;
|
const showDebugLog = false;
|
||||||
@@ -86,15 +121,15 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
// const emptyMark = `\u{2003}`;
|
// const emptyMark = `\u{2003}`;
|
||||||
function padLeftSpComputed(numI: ReactiveValue<number>, mark: string) {
|
function padLeftSpComputed(numI: ReactiveValue<number>, mark: string) {
|
||||||
const formatted = reactiveSource("");
|
const formatted = reactiveSource("");
|
||||||
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
|
let timer: number | undefined = undefined;
|
||||||
let maxLen = 1;
|
let maxLen = 1;
|
||||||
numI.onChanged((numX) => {
|
numI.onChanged((numX) => {
|
||||||
const num = numX.value;
|
const num = numX.value;
|
||||||
const numLen = `${Math.abs(num)}`.length + 1;
|
const numLen = `${Math.abs(num)}`.length + 1;
|
||||||
maxLen = maxLen < numLen ? numLen : maxLen;
|
maxLen = maxLen < numLen ? numLen : maxLen;
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) compatGlobal.clearTimeout(timer);
|
||||||
if (num == 0) {
|
if (num == 0) {
|
||||||
timer = setTimeout(() => {
|
timer = compatGlobal.setTimeout(() => {
|
||||||
formatted.value = "";
|
formatted.value = "";
|
||||||
maxLen = 1;
|
maxLen = 1;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
@@ -323,7 +358,7 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
if (this.nextFrameQueue) {
|
if (this.nextFrameQueue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.nextFrameQueue = requestAnimationFrame(() => {
|
this.nextFrameQueue = compatGlobal.requestAnimationFrame(() => {
|
||||||
this.nextFrameQueue = undefined;
|
this.nextFrameQueue = undefined;
|
||||||
const { message, status } = this.statusBarLabels.value;
|
const { message, status } = this.statusBarLabels.value;
|
||||||
// const recent = logMessages.value;
|
// const recent = logMessages.value;
|
||||||
@@ -346,7 +381,8 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
(a, b) => (a < b.ttl ? a : b.ttl),
|
(a, b) => (a < b.ttl ? a : b.ttl),
|
||||||
Number.MAX_SAFE_INTEGER
|
Number.MAX_SAFE_INTEGER
|
||||||
);
|
);
|
||||||
if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now);
|
if (this.logLines.length > 0)
|
||||||
|
compatGlobal.setTimeout(() => this.applyStatusBarText(), minimumNext - now);
|
||||||
const recent = this.logLines.map((e) => e.message);
|
const recent = this.logLines.map((e) => e.message);
|
||||||
const recentLogs = recent.reverse().join("\n");
|
const recentLogs = recent.reverse().join("\n");
|
||||||
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
|
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
|
||||||
@@ -368,7 +404,7 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
if (this.statusDiv) {
|
if (this.statusDiv) {
|
||||||
this.statusDiv.remove();
|
this.statusDiv.remove();
|
||||||
}
|
}
|
||||||
document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
|
compatGlobal.document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
_everyOnloadStart(): Promise<boolean> {
|
_everyOnloadStart(): Promise<boolean> {
|
||||||
@@ -390,7 +426,28 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
void this.services.API.showWindow(VIEW_TYPE_LOG);
|
void this.services.API.showWindow(VIEW_TYPE_LOG);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin));
|
this.addCommand({
|
||||||
|
id: "dump-debug-info",
|
||||||
|
name: "Generate full report for opening the issue with debug info",
|
||||||
|
callback: async () => {
|
||||||
|
const recentLog = [...logForDump];
|
||||||
|
const report = await generateReport(this.services.setting.currentSettings(), this.core);
|
||||||
|
const info = {
|
||||||
|
...report,
|
||||||
|
recentLog: recentLog.map(redactLog),
|
||||||
|
};
|
||||||
|
const yaml = `\`\`\`\`
|
||||||
|
# ---- Debug Info Dump ----
|
||||||
|
${stringifyYaml(info)}
|
||||||
|
\`\`\`\``;
|
||||||
|
if (await this.services.UI.promptCopyToClipboard("Debug info", yaml)) {
|
||||||
|
new Notice(
|
||||||
|
"Debug info copied to clipboard. You can paste it in the issue. Be careful as it may contain sensitive information, review it before sharing."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.registerView(VIEW_TYPE_LOG, (leaf: WorkspaceLeaf) => new LogPaneView(leaf, this.plugin));
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
@@ -404,7 +461,7 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
void this.setFileStatus();
|
void this.setFileStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const w = document.querySelectorAll(`.livesync-status`);
|
const w = compatGlobal.document.querySelectorAll(`.livesync-status`);
|
||||||
w.forEach((e) => e.remove());
|
w.forEach((e) => e.remove());
|
||||||
|
|
||||||
this.observeForLogs();
|
this.observeForLogs();
|
||||||
@@ -421,6 +478,8 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
this.statusBar?.addClass("syncstatusbar");
|
this.statusBar?.addClass("syncstatusbar");
|
||||||
}
|
}
|
||||||
this.adjustStatusDivPosition();
|
this.adjustStatusDivPosition();
|
||||||
|
this._log("Log module loaded", LOG_LEVEL_INFO);
|
||||||
|
this._log("Verbose log", LOG_LEVEL_VERBOSE);
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,11 +503,12 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let memoOnly = false;
|
||||||
if (level <= LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
|
if (level <= LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
|
||||||
return;
|
memoOnly = true;
|
||||||
}
|
}
|
||||||
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
|
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
|
||||||
return;
|
memoOnly = true;
|
||||||
}
|
}
|
||||||
const vaultName = this.services.vault.getVaultName();
|
const vaultName = this.services.vault.getVaultName();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -469,6 +529,15 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
? `${errorInfo}`
|
? `${errorInfo}`
|
||||||
: JSON.stringify(message, null, 2);
|
: JSON.stringify(message, null, 2);
|
||||||
const newMessage = timestamp + "->" + messageContent;
|
const newMessage = timestamp + "->" + messageContent;
|
||||||
|
|
||||||
|
if (this.settings?.writeLogToTheFile) {
|
||||||
|
this.writeLogToTheFile(now, vaultName, newMessage);
|
||||||
|
}
|
||||||
|
addLog(newMessage);
|
||||||
|
if (memoOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addDisplayLog(newMessage);
|
||||||
if (message instanceof Error) {
|
if (message instanceof Error) {
|
||||||
console.error(vaultName + ":" + newMessage);
|
console.error(vaultName + ":" + newMessage);
|
||||||
} else if (level >= LOG_LEVEL_INFO) {
|
} else if (level >= LOG_LEVEL_INFO) {
|
||||||
@@ -479,10 +548,6 @@ export class ModuleLog extends AbstractObsidianModule {
|
|||||||
if (!this.settings?.showOnlyIconsOnEditor) {
|
if (!this.settings?.showOnlyIconsOnEditor) {
|
||||||
this.statusLog.value = messageContent;
|
this.statusLog.value = messageContent;
|
||||||
}
|
}
|
||||||
if (this.settings?.writeLogToTheFile) {
|
|
||||||
this.writeLogToTheFile(now, vaultName, newMessage);
|
|
||||||
}
|
|
||||||
addLog(newMessage);
|
|
||||||
this.logLines.push({ ttl: now.getTime() + 3000, message: newMessage });
|
this.logLines.push({ ttl: now.getTime() + 3000, message: newMessage });
|
||||||
|
|
||||||
if (level >= LOG_LEVEL_NOTICE) {
|
if (level >= LOG_LEVEL_NOTICE) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
|
|||||||
import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts";
|
import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
import type { PageFunctions } from "./SettingPane.ts";
|
import type { PageFunctions } from "./SettingPane.ts";
|
||||||
|
import { generateReport } from "@/common/reportTool.ts";
|
||||||
export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||||
// const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
// const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
|
||||||
// hatchWarn.addClass("op-warn-info");
|
// hatchWarn.addClass("op-warn-info");
|
||||||
@@ -69,140 +70,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
|||||||
eventHub.emitEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE);
|
eventHub.emitEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(paneEl).setName($msg("Prepare the 'report' to create an issue")).addButton((button) =>
|
new Setting(paneEl).setName($msg("Prepare the 'report' to create an issue")).addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText($msg("Copy Report to clipboard"))
|
.setButtonText($msg("Copy Report to clipboard"))
|
||||||
.setCta()
|
.setCta()
|
||||||
.setDisabled(false)
|
.setDisabled(false)
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
let responseConfig: any = {};
|
await this.app.commands.executeCommandById("obsidian-livesync:dump-debug-info");
|
||||||
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
|
|
||||||
if (this.editingSettings.remoteType == REMOTE_COUCHDB) {
|
|
||||||
try {
|
|
||||||
const credential = generateCredentialObject(this.editingSettings);
|
|
||||||
const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders);
|
|
||||||
const r = await requestToCouchDBWithCredentials(
|
|
||||||
this.editingSettings.couchDB_URI,
|
|
||||||
credential,
|
|
||||||
window.origin,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
customHeaders
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger(JSON.stringify(r.json, null, 2));
|
|
||||||
|
|
||||||
responseConfig = r.json;
|
|
||||||
responseConfig["couch_httpd_auth"].secret = REDACTED;
|
|
||||||
responseConfig["couch_httpd_auth"].authentication_db = REDACTED;
|
|
||||||
responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED;
|
|
||||||
responseConfig["couchdb"].uuid = REDACTED;
|
|
||||||
responseConfig["admins"] = REDACTED;
|
|
||||||
delete responseConfig["jwt_keys"];
|
|
||||||
if ("secret" in responseConfig["chttpd_auth"])
|
|
||||||
responseConfig["chttpd_auth"].secret = REDACTED;
|
|
||||||
} catch (ex) {
|
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
|
||||||
responseConfig = {
|
|
||||||
error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (this.editingSettings.remoteType == REMOTE_MINIO) {
|
|
||||||
responseConfig = { error: "Object Storage Synchronisation" };
|
|
||||||
//
|
|
||||||
}
|
|
||||||
const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[];
|
|
||||||
const pluginConfig = JSON.parse(JSON.stringify(this.editingSettings)) as ObsidianLiveSyncSettings;
|
|
||||||
const pluginKeys = Object.keys(pluginConfig);
|
|
||||||
for (const key of pluginKeys) {
|
|
||||||
if (defaultKeys.includes(key as any)) continue;
|
|
||||||
delete pluginConfig[key as keyof ObsidianLiveSyncSettings];
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginConfig.couchDB_DBNAME = REDACTED;
|
|
||||||
pluginConfig.couchDB_PASSWORD = REDACTED;
|
|
||||||
const scheme = pluginConfig.couchDB_URI.startsWith("http:")
|
|
||||||
? "(HTTP)"
|
|
||||||
: pluginConfig.couchDB_URI.startsWith("https:")
|
|
||||||
? "(HTTPS)"
|
|
||||||
: "";
|
|
||||||
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI)
|
|
||||||
? "cloudant"
|
|
||||||
: `self-hosted${scheme}`;
|
|
||||||
pluginConfig.couchDB_USER = REDACTED;
|
|
||||||
pluginConfig.passphrase = REDACTED;
|
|
||||||
pluginConfig.encryptedPassphrase = REDACTED;
|
|
||||||
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
|
||||||
pluginConfig.accessKey = REDACTED;
|
|
||||||
pluginConfig.secretKey = REDACTED;
|
|
||||||
const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
|
|
||||||
const toSchemeOnly = (uri: string) => {
|
|
||||||
try {
|
|
||||||
return `${new URL(uri).protocol}//`;
|
|
||||||
} catch {
|
|
||||||
const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//);
|
|
||||||
return matched?.[0] ?? REDACTED;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
pluginConfig.remoteConfigurations = Object.fromEntries(
|
|
||||||
Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
...config,
|
|
||||||
uri: toSchemeOnly(config.uri),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
pluginConfig.region = redact(pluginConfig.region);
|
|
||||||
pluginConfig.bucket = redact(pluginConfig.bucket);
|
|
||||||
pluginConfig.pluginSyncExtendedSetting = {};
|
|
||||||
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
|
|
||||||
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
|
|
||||||
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
|
|
||||||
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
|
|
||||||
pluginConfig.jwtKey = redact(pluginConfig.jwtKey);
|
|
||||||
pluginConfig.jwtSub = redact(pluginConfig.jwtSub);
|
|
||||||
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
|
|
||||||
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
|
|
||||||
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
|
|
||||||
pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential);
|
|
||||||
pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername);
|
|
||||||
pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`;
|
|
||||||
const endpoint = pluginConfig.endpoint;
|
|
||||||
if (endpoint == "") {
|
|
||||||
pluginConfig.endpoint = "Not configured or AWS";
|
|
||||||
} else {
|
|
||||||
const endpointScheme = pluginConfig.endpoint.startsWith("http:")
|
|
||||||
? "(HTTP)"
|
|
||||||
: pluginConfig.endpoint.startsWith("https:")
|
|
||||||
? "(HTTPS)"
|
|
||||||
: "";
|
|
||||||
pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`;
|
|
||||||
}
|
|
||||||
const obsidianInfo = {
|
|
||||||
navigator: navigator.userAgent,
|
|
||||||
fileSystem: this.core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
|
|
||||||
};
|
|
||||||
const msgConfig = `# ---- Obsidian info ----
|
|
||||||
${stringifyYaml(obsidianInfo)}
|
|
||||||
---
|
|
||||||
# ---- remote config ----
|
|
||||||
${stringifyYaml(responseConfig)}
|
|
||||||
---
|
|
||||||
# ---- Plug-in config ----
|
|
||||||
${stringifyYaml({
|
|
||||||
version: this.manifestVersion,
|
|
||||||
...pluginConfig,
|
|
||||||
})}`;
|
|
||||||
console.log(msgConfig);
|
|
||||||
if ((await this.services.UI.promptCopyToClipboard("Generated report", msgConfig)) == true) {
|
|
||||||
// await navigator.clipboard.writeText(msgConfig);
|
|
||||||
// Logger(
|
|
||||||
// `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`,
|
|
||||||
// LOG_LEVEL_NOTICE
|
|
||||||
// );
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
new Setting(paneEl)
|
new Setting(paneEl)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Locator, Page } from "playwright";
|
||||||
|
import { type ObsidianHandle, launchObsidian } from "./obsidian";
|
||||||
|
import { type VaultSettingsOptions, type VaultSetupResult, setupTestVaultWithSettings } from "./vault";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers (vault setup, test scaffolding, etc.)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export async function withSeededVault(
|
||||||
|
options: VaultSettingsOptions,
|
||||||
|
run: (context: { app: ObsidianHandle; vault: VaultSetupResult }) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const vault = setupTestVaultWithSettings(options);
|
||||||
|
const app = await launchObsidian(vault.fakeAppData, vault.vaultDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await run({ app, vault });
|
||||||
|
} finally {
|
||||||
|
await app.close().catch(() => {});
|
||||||
|
vault.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selectors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** CSS selector for the settings-tab content area. */
|
||||||
|
export const SELECTOR_SETTINGS_CONTENT = ".vertical-tab-content-container";
|
||||||
|
|
||||||
|
/** CSS selector for Obsidian notice toasts. */
|
||||||
|
export const SELECTOR_NOTICE = ".notice-container .notice";
|
||||||
|
|
||||||
|
export function locateModalByTitle(page: Page, title: string): Locator {
|
||||||
|
return page.locator(".modal-container .modal-title").filter({ hasText: title });
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/* eslint-disable obsidianmd/prefer-window-timers */
|
||||||
|
/* eslint-disable import/no-nodejs-modules */
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
/**
|
||||||
|
* helpers/obsidian.ts
|
||||||
|
*
|
||||||
|
* Launch / teardown helpers for the Obsidian Electron application and
|
||||||
|
* common UI interactions needed across test files.
|
||||||
|
*
|
||||||
|
* Launch strategy
|
||||||
|
* ---------------
|
||||||
|
* Playwright's `_electron.launch()` cannot reliably connect to Obsidian.exe
|
||||||
|
* via CDP because Obsidian's startup sequence does not expose the DevTools
|
||||||
|
* URL on stdout/stderr in a way Playwright can detect. Instead, we:
|
||||||
|
* 1. Spawn Obsidian with a fixed `--remote-debugging-port`.
|
||||||
|
* 2. Poll `http://127.0.0.1:<port>/json/version` until the port is ready.
|
||||||
|
* 3. Connect with `chromium.connectOverCDP()`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import http from "node:http";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
import type { Browser, Page } from "playwright";
|
||||||
|
import type { ChildProcess } from "node:child_process";
|
||||||
|
import process from "node:process";
|
||||||
|
import { enablePlugin, isPluginEnabled } from "./obsidianFunctions";
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Executable path resolution
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function defaultObsidianPath(): string {
|
||||||
|
switch (os.platform()) {
|
||||||
|
case "win32":
|
||||||
|
return path.join(os.homedir(), "AppData", "Local", "Obsidian", "Obsidian.exe");
|
||||||
|
case "darwin":
|
||||||
|
return "/Applications/Obsidian.app/Contents/MacOS/Obsidian";
|
||||||
|
default:
|
||||||
|
return process.env["OBSIDIAN_PATH"] ?? "/usr/bin/obsidian";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the Obsidian executable.
|
||||||
|
* Override with the `OBSIDIAN_PATH` environment variable if needed.
|
||||||
|
*/
|
||||||
|
export const OBSIDIAN_EXECUTABLE: string = process.env["OBSIDIAN_PATH"] ?? defaultObsidianPath();
|
||||||
|
|
||||||
|
/** Fixed CDP port used for all test runs (workers: 1, so no collisions). */
|
||||||
|
const CDP_PORT = 19222;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Launch
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle returned by `launchObsidian`. Provides just enough surface to drive
|
||||||
|
* the Obsidian window and shut it down cleanly.
|
||||||
|
*/
|
||||||
|
export interface ObsidianHandle {
|
||||||
|
/** Returns the main Obsidian renderer page. */
|
||||||
|
firstWindow(): Promise<Page>;
|
||||||
|
/** Closes the CDP connection and kills the Obsidian process. */
|
||||||
|
close(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Poll `http://127.0.0.1:<port>/json/version` until Obsidian is ready. */
|
||||||
|
async function waitForCDP(port: number, timeoutMs: number): Promise<void> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const ready = await new Promise<boolean>((resolve) => {
|
||||||
|
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res: http.IncomingMessage) => {
|
||||||
|
res.resume();
|
||||||
|
resolve(res.statusCode === 200);
|
||||||
|
});
|
||||||
|
req.on("error", () => resolve(false));
|
||||||
|
req.setTimeout(1_000, () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (ready) return;
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
throw new Error(`Obsidian CDP port ${port} was not ready within ${timeoutMs}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches Obsidian with an isolated user-data directory and opens the
|
||||||
|
* given vault via the `obsidian://open` URI scheme.
|
||||||
|
*
|
||||||
|
* Uses a fixed `--remote-debugging-port` so we can poll and connect via
|
||||||
|
* `chromium.connectOverCDP()` without relying on Playwright's electron
|
||||||
|
* startup detection, which does not work with Obsidian.exe.
|
||||||
|
*/
|
||||||
|
export async function launchObsidian(fakeAppData: string, vaultDir: string): Promise<ObsidianHandle> {
|
||||||
|
const proc: ChildProcess = spawn(
|
||||||
|
OBSIDIAN_EXECUTABLE,
|
||||||
|
[
|
||||||
|
`--remote-debugging-port=${CDP_PORT}`,
|
||||||
|
`--user-data-dir=${fakeAppData}`,
|
||||||
|
"--no-sandbox",
|
||||||
|
"--lang=en",
|
||||||
|
`obsidian://open?path=${encodeURIComponent(vaultDir)}`,
|
||||||
|
],
|
||||||
|
{ env: { ...process.env, LIBGL_ALWAYS_SOFTWARE: "1" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
proc.on("error", (err: Error) => {
|
||||||
|
console.error("[launchObsidian] spawn error:", err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForCDP(CDP_PORT, 60_000);
|
||||||
|
|
||||||
|
const browser: Browser = await chromium.connectOverCDP(`http://127.0.0.1:${CDP_PORT}`);
|
||||||
|
const waitForProcessExit = async (): Promise<void> => {
|
||||||
|
if (proc.exitCode !== null || proc.killed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
proc.removeListener("exit", onExit);
|
||||||
|
proc.removeListener("close", onExit);
|
||||||
|
resolve();
|
||||||
|
}, 5_000);
|
||||||
|
|
||||||
|
const onExit = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
proc.removeListener("exit", onExit);
|
||||||
|
proc.removeListener("close", onExit);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.once("exit", onExit);
|
||||||
|
proc.once("close", onExit);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
close: async () => {
|
||||||
|
try {
|
||||||
|
await browser.close();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
proc.kill();
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
await waitForProcessExit();
|
||||||
|
},
|
||||||
|
firstWindow: async (): Promise<Page> => {
|
||||||
|
const deadline = Date.now() + 30_000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
for (const ctx of browser.contexts()) {
|
||||||
|
const pages = ctx.pages().filter((p: Page) => !p.isClosed());
|
||||||
|
if (pages.length > 0) return pages[0];
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
}
|
||||||
|
throw new Error("No Obsidian window found after 30s");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Window helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the main Obsidian window and waits for its DOM to be ready.
|
||||||
|
*/
|
||||||
|
export async function getMainWindow(app: ObsidianHandle): Promise<Page> {
|
||||||
|
const page = await app.firstWindow();
|
||||||
|
await page.waitForLoadState("domcontentloaded", { timeout: 30_000 });
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until the Obsidian vault workspace has finished loading.
|
||||||
|
*
|
||||||
|
* Handles the 'Trust author and enable plugins' prompt and the
|
||||||
|
* community-plugins information modal that appear on a first-time vault open.
|
||||||
|
*/
|
||||||
|
export async function waitForVaultReady(page: Page): Promise<void> {
|
||||||
|
// Trust prompt — must be dismissed before the workspace renders.
|
||||||
|
const trustButton = page.getByRole("button", { name: /trust author and enable plugins/i });
|
||||||
|
try {
|
||||||
|
await trustButton.waitFor({ state: "visible", timeout: 15_000 });
|
||||||
|
await trustButton.click();
|
||||||
|
await page.waitForTimeout(1_500);
|
||||||
|
} catch {
|
||||||
|
// Not shown — vault already trusted or safe mode off.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the trust prompt is handled, then the plugin dialogues may appear. Wait a bit for them to show up and log them if they do, to help diagnose blocked flows.
|
||||||
|
|
||||||
|
// await page.waitForTimeout(100);
|
||||||
|
// Community-plugins modal — dismiss with Escape.
|
||||||
|
try {
|
||||||
|
const modal = page.locator(".modal-container").filter({ hasText: /community plugins/i });
|
||||||
|
await modal.waitFor({ state: "visible", timeout: 5_000 });
|
||||||
|
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
await page.waitForTimeout(10);
|
||||||
|
} catch {
|
||||||
|
// Modal not shown.
|
||||||
|
}
|
||||||
|
await page.waitForSelector(".workspace-ribbon", { timeout: 60_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enablePluginInObsidian(page: Page, pluginName: string) {
|
||||||
|
const handled = await page.evaluateHandle(enablePlugin, pluginName);
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
export function isPluginEnabledInObsidian(page: Page, pluginName: string): Promise<boolean> {
|
||||||
|
const handled = page.evaluate(isPluginEnabled, pluginName);
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Settings modal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the Obsidian Settings modal via the standard keyboard shortcut and
|
||||||
|
* waits for the navigation panel to become visible.
|
||||||
|
*/
|
||||||
|
export async function openSettings(page: Page): Promise<void> {
|
||||||
|
await page.keyboard.press("Control+,");
|
||||||
|
await page.waitForSelector(".modal-container .vertical-tab-nav-item", { timeout: 15_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks a settings navigation tab identified by its visible text label.
|
||||||
|
*/
|
||||||
|
export async function clickSettingsTab(page: Page, label: string): Promise<void> {
|
||||||
|
const tab = page.locator(".vertical-tab-nav-item", { hasText: label });
|
||||||
|
await tab.first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Settings and navigates directly to the Self-hosted LiveSync tab.
|
||||||
|
*/
|
||||||
|
export async function openLiveSyncSettings(page: Page): Promise<void> {
|
||||||
|
await openSettings(page);
|
||||||
|
await clickSettingsTab(page, "Self-hosted LiveSync");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs visible modal/dialog-like UI elements to help diagnose blocked flows.
|
||||||
|
*/
|
||||||
|
export async function logVisibleDialogs(page: Page, label = "dialogs"): Promise<void> {
|
||||||
|
const summaries = await page
|
||||||
|
.locator(".modal-container, [role='dialog'], .notice-container .notice")
|
||||||
|
.evaluateAll((nodes) => {
|
||||||
|
return nodes
|
||||||
|
.map((node) => {
|
||||||
|
const element = node as HTMLElement;
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const visible =
|
||||||
|
style.display !== "none" &&
|
||||||
|
style.visibility !== "hidden" &&
|
||||||
|
rect.width > 0 &&
|
||||||
|
rect.height > 0 &&
|
||||||
|
!!element.textContent?.trim();
|
||||||
|
|
||||||
|
if (!visible) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
classes: element.className,
|
||||||
|
text: element.textContent?.replace(/\s+/g, " ").trim().slice(0, 240) ?? "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((item): item is { classes: string; text: string } => !!item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (summaries.length === 0) {
|
||||||
|
console.log(`[obsidian:${label}] no visible dialogs`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, summary] of summaries.entries()) {
|
||||||
|
console.log(`[obsidian:${label}] #${index + 1} class=${summary.classes} text=${summary.text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
import type { App } from "obsidian";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var app: App & {
|
||||||
|
plugins: {
|
||||||
|
enabledPlugins: Set<string>;
|
||||||
|
enablePlugin: (name: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enablePlugin = async (pluginName: string) => {
|
||||||
|
return await window.app.plugins.enablePlugin(pluginName);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPluginEnabled = (pluginName: string) => {
|
||||||
|
return window.app.plugins.enabledPlugins.has(pluginName);
|
||||||
|
};
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
/* eslint-disable obsidianmd/prefer-window-timers */
|
||||||
|
// This file is a test helper and is allowed to use Node.js modules.
|
||||||
|
/* eslint-disable obsidianmd/hardcoded-config-path */
|
||||||
|
// This file is a test helper and is allowed to use Node.js modules.
|
||||||
|
/* eslint-disable import/no-nodejs-modules */
|
||||||
|
/**
|
||||||
|
* helpers/vault.ts
|
||||||
|
*
|
||||||
|
* Creates a fully-isolated, throwaway Obsidian vault for each test run.
|
||||||
|
*
|
||||||
|
* Directory layout produced by `setupTestVault()`:
|
||||||
|
*
|
||||||
|
* <tmpdir>/livesync-e2e-<id>/
|
||||||
|
* obsidian.json <- registered vault list (Obsidian userData config)
|
||||||
|
* vault/
|
||||||
|
* .obsidian/
|
||||||
|
* app.json <- safe-mode disabled
|
||||||
|
* community-plugins.json
|
||||||
|
* plugins/
|
||||||
|
* obsidian-livesync/
|
||||||
|
* main.js <- built plugin (copied from repo root)
|
||||||
|
* manifest.json
|
||||||
|
* styles.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
|
||||||
|
/** Absolute path to the repository root (two levels above helpers/). */
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const REPO_ROOT = path.resolve(__dirname, "../..");
|
||||||
|
|
||||||
|
export interface VaultSetupResult {
|
||||||
|
/** The vault directory that Obsidian will open. */
|
||||||
|
vaultDir: string;
|
||||||
|
/**
|
||||||
|
* The directory used as `--user-data-dir` for the Obsidian process.
|
||||||
|
* Obsidian reads its vault registry from `<fakeAppData>/obsidian.json`.
|
||||||
|
*/
|
||||||
|
fakeAppData: string;
|
||||||
|
/** Removes the entire temporary tree. */
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VaultSettingsOptions {
|
||||||
|
/** Optional custom app.json content under <vault>/.obsidian/app.json */
|
||||||
|
appJson?: Record<string, unknown>;
|
||||||
|
/** Community plugin IDs to mark as enabled. */
|
||||||
|
communityPlugins?: string[];
|
||||||
|
/** Per-plugin configuration keyed by plugin ID. */
|
||||||
|
pluginData?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a throw-away vault with the built plugin pre-installed and
|
||||||
|
* registered in an isolated Obsidian configuration directory.
|
||||||
|
*
|
||||||
|
* Call `cleanup()` (or use `test.afterAll`) to delete the temporary files.
|
||||||
|
*/
|
||||||
|
export function setupTestVault(): VaultSetupResult {
|
||||||
|
return setupTestVaultWithSettings({});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a throw-away vault with optional initial Obsidian/plugin settings.
|
||||||
|
*
|
||||||
|
* This helper is intended for real-Obsidian e2e tests that need to open a
|
||||||
|
* vault in a known configuration state.
|
||||||
|
*/
|
||||||
|
export function setupTestVaultWithSettings(options: VaultSettingsOptions = {}): VaultSetupResult {
|
||||||
|
const id = randomBytes(4).toString("hex");
|
||||||
|
const baseDir = path.join(os.tmpdir(), `livesync-e2e-${id}`);
|
||||||
|
const fakeAppData = baseDir;
|
||||||
|
const vaultDir = path.join(baseDir, "vault");
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ vault
|
||||||
|
const dotObsidian = path.join(vaultDir, ".obsidian");
|
||||||
|
const pluginDir = path.join(dotObsidian, "plugins", "obsidian-livesync");
|
||||||
|
mkdirSync(pluginDir, { recursive: true });
|
||||||
|
|
||||||
|
// Copy the built plugin artefacts from the repository root.
|
||||||
|
for (const file of ["main.js", "manifest.json", "styles.css"]) {
|
||||||
|
const src = path.join(REPO_ROOT, file);
|
||||||
|
if (existsSync(src)) {
|
||||||
|
copyFileSync(src, path.join(pluginDir, file));
|
||||||
|
} else {
|
||||||
|
console.warn(`[vault setup] Expected file not found: ${src}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable Obsidian safe mode so community plugins are allowed to load.
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dotObsidian, "app.json"),
|
||||||
|
JSON.stringify({ promptDelete: false, ...(options.appJson ?? {}) }, null, 2),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tell Obsidian which community plugins are enabled.
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dotObsidian, "community-plugins.json"),
|
||||||
|
// JSON.stringify(options.communityPlugins ?? ["obsidian-livesync"], null, 2),
|
||||||
|
// You should enable the plugin(s) explicitly
|
||||||
|
JSON.stringify(options.communityPlugins ?? [], null, 2),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.pluginData) {
|
||||||
|
for (const [pluginId, value] of Object.entries(options.pluginData)) {
|
||||||
|
const target = path.join(dotObsidian, "plugins", pluginId, "data.json");
|
||||||
|
mkdirSync(path.dirname(target), { recursive: true });
|
||||||
|
writeFileSync(target, JSON.stringify(value, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------ Obsidian global config
|
||||||
|
// With --user-data-dir=<fakeAppData>, Obsidian reads its vault registry
|
||||||
|
// directly from <fakeAppData>/obsidian.json.
|
||||||
|
mkdirSync(fakeAppData, { recursive: true });
|
||||||
|
|
||||||
|
const vaultId = randomBytes(8).toString("hex");
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
path.join(fakeAppData, "obsidian.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
vaults: {
|
||||||
|
[vaultId]: {
|
||||||
|
path: vaultDir,
|
||||||
|
ts: Date.now(),
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateDisabled: true,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vaultDir,
|
||||||
|
fakeAppData,
|
||||||
|
cleanup: () =>
|
||||||
|
void (async () => {
|
||||||
|
for (let attempt = 1; attempt <= 5; attempt++) {
|
||||||
|
try {
|
||||||
|
rmSync(baseDir, { recursive: true, force: true });
|
||||||
|
console.log(`[vault cleanup] Successfully removed temporary directory: ${baseDir}`);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
`[vault cleanup] Attempt ${attempt} failed to remove temporary directory: ${baseDir}`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`[vault cleanup] Failed to remove temporary directory after multiple attempts: ${baseDir}`
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Example wrapper for Playwright test functions and assertions, this file is not used in Self-hosted LiveSync.
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
export { test, expect } from "playwright/test";
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from "playwright/test";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: path.join(__dirname, "tests"),
|
||||||
|
outputDir: path.join(__dirname, "test-results"),
|
||||||
|
|
||||||
|
// Each test may need to cold-start Obsidian and wait for the vault to load.
|
||||||
|
timeout: 120_000,
|
||||||
|
expect: { timeout: 20_000 },
|
||||||
|
|
||||||
|
// Tests are stateful (one Obsidian process per test file), so no parallelism.
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
retries: 0,
|
||||||
|
|
||||||
|
reporter: [["list"], ["html", { open: "never", outputFolder: path.join(__dirname, "playwright-report") }]],
|
||||||
|
use: {
|
||||||
|
// Artefacts are kept only when a test fails.
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* tests/sample.spec.ts
|
||||||
|
*
|
||||||
|
* Example e2e test that opens a vault with pre-seeded settings.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getMainWindow,
|
||||||
|
waitForVaultReady,
|
||||||
|
enablePluginInObsidian,
|
||||||
|
isPluginEnabledInObsidian,
|
||||||
|
} from "../helpers/obsidian";
|
||||||
|
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
|
||||||
|
import { PartialMessages } from "@lib/common/messages/def";
|
||||||
|
import { locateModalByTitle, withSeededVault } from "test_e2e/helpers/helpers";
|
||||||
|
import { test, expect } from "test_e2e/helpers/wrapper";
|
||||||
|
const def = PartialMessages.def;
|
||||||
|
|
||||||
|
test("show Welcome when isConfigured is false", async () => {
|
||||||
|
await withSeededVault(
|
||||||
|
{
|
||||||
|
appJson: {
|
||||||
|
promptDelete: false,
|
||||||
|
},
|
||||||
|
communityPlugins: [],
|
||||||
|
pluginData: {
|
||||||
|
"obsidian-livesync": {
|
||||||
|
deviceAndVaultName: "e2e-configured-device",
|
||||||
|
isConfigured: true,
|
||||||
|
notifyThresholdOfRemoteStorageSize: 10000,
|
||||||
|
} satisfies Partial<ObsidianLiveSyncSettings>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ app }) => {
|
||||||
|
const page = await getMainWindow(app);
|
||||||
|
|
||||||
|
await waitForVaultReady(page);
|
||||||
|
await expect(enablePluginInObsidian(page, "obsidian-livesync")).resolves.not.toThrow();
|
||||||
|
expect(isPluginEnabledInObsidian(page, "obsidian-livesync")).toBeTruthy();
|
||||||
|
const welcome = locateModalByTitle(page, def["moduleMigration.titleWelcome"]);
|
||||||
|
await expect(welcome).toBeHidden({ timeout: 1_000 });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not show Welcome when isConfigured is true", async () => {
|
||||||
|
await withSeededVault(
|
||||||
|
{
|
||||||
|
appJson: {
|
||||||
|
promptDelete: false,
|
||||||
|
},
|
||||||
|
communityPlugins: [],
|
||||||
|
pluginData: {
|
||||||
|
"obsidian-livesync": {
|
||||||
|
deviceAndVaultName: "e2e-configured-device",
|
||||||
|
isConfigured: true,
|
||||||
|
notifyThresholdOfRemoteStorageSize: 10000,
|
||||||
|
} satisfies Partial<ObsidianLiveSyncSettings>,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ app }) => {
|
||||||
|
const page = await getMainWindow(app);
|
||||||
|
await waitForVaultReady(page);
|
||||||
|
await expect(enablePluginInObsidian(page, "obsidian-livesync")).resolves.not.toThrow();
|
||||||
|
expect(isPluginEnabledInObsidian(page, "obsidian-livesync")).toBeTruthy();
|
||||||
|
const welcome = locateModalByTitle(page, def["moduleMigration.titleWelcome"]);
|
||||||
|
await expect(welcome).toBeHidden({ timeout: 1_000 });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
+14
-3
@@ -3,12 +3,23 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
|
|||||||
|
|
||||||
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
|
||||||
|
|
||||||
## Unreleased (0.25.64-patch1)
|
## 0.25.65
|
||||||
|
|
||||||
18th May, 2026
|
19th May, 2026
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix an issue about resuming from background on iOS (#888).
|
||||||
|
- Now Chunk Splitter: `V3: Fine Deduplication` is working fine again (#866).
|
||||||
|
- It has some drawbacks, such as fewer chunks are generated. However, it makes less transfer and storage when the files are modified but not completely changed.
|
||||||
|
- Unsynchronised local changes (which means changes that have not been sent) are now correctly preserved as a conflict (Thank you so much for @SeleiXi!).
|
||||||
|
- Avoid creating a new revision when the current and conflicted revisions have identical content (Thank you so much for @daichi-629).
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
- Improved an error verbosity on concurrent processing on start-up process.
|
- Improved the error verbosity on concurrent processing during the start-up process.
|
||||||
|
- Now the `report` includes recent logs (of verbosity `verbose` even settings is not set to `verbose`).
|
||||||
|
- Updating logs is now debounced to avoid excessive updates during rapid log generation.
|
||||||
|
- Added a `Generate full report for opening the issue with debug info` command to the command palette, which generates a report without opening the settings dialogue.
|
||||||
|
|
||||||
|
|
||||||
## 0.25.64
|
## 0.25.64
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import { glob } from "glob";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { promises as fs } from "node:fs";
|
||||||
|
import { isPlainText, shouldSplitAsPlainText } from "../../src/lib/src/string_and_binary/path";
|
||||||
|
import { splitPiecesRabinKarp } from "../../src/lib/src/string_and_binary/chunks";
|
||||||
|
import {
|
||||||
|
PREFERRED_BASE,
|
||||||
|
PREFERRED_JOURNAL_SYNC,
|
||||||
|
PREFERRED_SETTING_CLOUDANT,
|
||||||
|
PREFERRED_SETTING_SELF_HOSTED,
|
||||||
|
} from "../../src/lib/src/common/models/setting.const.preferred";
|
||||||
|
import { type ObsidianLiveSyncSettings, DEFAULT_SETTINGS, MAX_DOC_SIZE_BIN } from "../../src/lib/src/common/types";
|
||||||
|
|
||||||
|
async function blobFromString(content: string): Promise<Blob> {
|
||||||
|
return new Blob([content], { type: "text/plain" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferred = PREFERRED_BASE;
|
||||||
|
const preferredJournal = PREFERRED_JOURNAL_SYNC;
|
||||||
|
const preferredCouchDB = PREFERRED_SETTING_SELF_HOSTED;
|
||||||
|
const preferredIBM = PREFERRED_SETTING_CLOUDANT;
|
||||||
|
|
||||||
|
function computeChunkSize(overlay: Partial<ObsidianLiveSyncSettings>) {
|
||||||
|
const settings = { ...DEFAULT_SETTINGS, ...overlay };
|
||||||
|
const maxChunkSize = Math.floor(MAX_DOC_SIZE_BIN * ((settings.customChunkSize || 0) * 1 + 1));
|
||||||
|
const pieceSize = maxChunkSize;
|
||||||
|
|
||||||
|
const minimumChunkSize = settings.minimumChunkSize;
|
||||||
|
return { pieceSize, minimumChunkSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testSplit(
|
||||||
|
splitPiecesRabinKarpFn: typeof splitPiecesRabinKarp,
|
||||||
|
content: Blob,
|
||||||
|
settingsOverlay: Partial<ObsidianLiveSyncSettings>
|
||||||
|
) {
|
||||||
|
const { pieceSize, minimumChunkSize } = computeChunkSize(settingsOverlay);
|
||||||
|
const isPlain = content.type === "text/plain";
|
||||||
|
const chunkGenerator = await splitPiecesRabinKarpFn(content, pieceSize, isPlain, minimumChunkSize);
|
||||||
|
const chunks = [] as string[];
|
||||||
|
for await (const chunk of chunkGenerator()) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
// if there are few chunks, calculate average chunk size except the last chunk which can be smaller due to the way the algorithm works, especially for small files.
|
||||||
|
const averageChunkSize =
|
||||||
|
chunks.length > 1
|
||||||
|
? chunks.slice(0, -1).reduce((acc, chunk) => acc + chunk.length, 0) / (chunks.length - 1)
|
||||||
|
: chunks.reduce((acc, chunk) => acc + chunk.length, 0) / chunks.length;
|
||||||
|
const lastChunk = chunks[chunks.length - 1];
|
||||||
|
// compute minimum chunk size if the last chunk is not the smallest.
|
||||||
|
const nonLastChunkSizes = chunks.slice(0, -1).map((c) => c.length);
|
||||||
|
const minChunkSize = nonLastChunkSizes.length > 0 ? Math.min(...nonLastChunkSizes) : lastChunk.length;
|
||||||
|
const result = {
|
||||||
|
isPlain,
|
||||||
|
originalSize: content.size,
|
||||||
|
chunkCount: chunks.length,
|
||||||
|
totalLength: chunks.reduce((acc, chunk) => acc + chunk.length, 0),
|
||||||
|
averageChunkSize: averageChunkSize,
|
||||||
|
maxChunkSize: Math.max(...chunks.map((c) => c.length)),
|
||||||
|
minChunkSize: minChunkSize,
|
||||||
|
uniqueChunks: new Set(chunks).size,
|
||||||
|
chunks: chunks,
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = resolve(__filename, "..");
|
||||||
|
async function loadFileAsBlob(filePath: string): Promise<Blob> {
|
||||||
|
if (shouldSplitAsPlainText(filePath)) {
|
||||||
|
const content = await fs.readFile(filePath, "utf-8");
|
||||||
|
return blobFromString(content);
|
||||||
|
} else {
|
||||||
|
const buffer = await fs.readFile(filePath);
|
||||||
|
return new Blob([buffer]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const testProfiles = [
|
||||||
|
{ name: "CouchDB", settings: preferredCouchDB },
|
||||||
|
{ name: "IBM Cloudant", settings: preferredIBM },
|
||||||
|
{ name: "Journal Sync", settings: preferredJournal },
|
||||||
|
// { name: "Base", settings: preferred },
|
||||||
|
];
|
||||||
|
function modifyBlob(blob: Blob, position: number, insertText: string): Blob {
|
||||||
|
const before = blob.slice(0, position);
|
||||||
|
const after = blob.slice(position);
|
||||||
|
const insert = new Blob([insertText], { type: blob.type });
|
||||||
|
return new Blob([before, insert, after], { type: blob.type });
|
||||||
|
}
|
||||||
|
async function main() {
|
||||||
|
const results = [] as string[][];
|
||||||
|
console.log("directory:", __dirname);
|
||||||
|
const findPath = resolve(__dirname, "../../");
|
||||||
|
console.warn("CWD:", findPath);
|
||||||
|
let testFiles = await glob("**/*.*", {
|
||||||
|
cwd: findPath,
|
||||||
|
maxDepth: 20,
|
||||||
|
ignore: ["**/node_modules/**", "**/.obsidian/**", "**/dist/**", "**/build/**", "**/out/**"],
|
||||||
|
});
|
||||||
|
testFiles = testFiles.filter((file) => {
|
||||||
|
const ext = file.split(".").pop()?.toLowerCase() || "";
|
||||||
|
return ["md", "txt", "json", "csv", "png"].includes(ext);
|
||||||
|
});
|
||||||
|
const header = [
|
||||||
|
"Profile",
|
||||||
|
"Implementation",
|
||||||
|
"Edition",
|
||||||
|
"File",
|
||||||
|
"Mode",
|
||||||
|
"Original Size (bytes)",
|
||||||
|
"Chunk Count",
|
||||||
|
"Average Chunk Size",
|
||||||
|
"Max Chunk Size",
|
||||||
|
"Min Chunk Size",
|
||||||
|
"Unique Chunks",
|
||||||
|
"Shared Chunks",
|
||||||
|
"Savings",
|
||||||
|
"Newly added (count)",
|
||||||
|
"Newly consumed (bytes)",
|
||||||
|
];
|
||||||
|
for (const profile of testProfiles) {
|
||||||
|
console.log(`Testing profile: ${profile.name}`);
|
||||||
|
for (const fn of [splitPiecesRabinKarp]) {
|
||||||
|
const funcProfile = fn !== splitPiecesRabinKarp ? "Old" : "New";
|
||||||
|
console.log(`Testing function: ${funcProfile}`);
|
||||||
|
for (const file of testFiles) {
|
||||||
|
const filePath = resolve(findPath, file);
|
||||||
|
const isPlain = shouldSplitAsPlainText(filePath);
|
||||||
|
const content = await loadFileAsBlob(filePath);
|
||||||
|
console.log(`Testing file: ${file} (size: ${content.size} bytes)`);
|
||||||
|
const result = await testSplit(fn, content, profile.settings);
|
||||||
|
const chunkSizes = result.chunks.map((c) => c.length);
|
||||||
|
const savings = result.originalSize - chunkSizes.reduce((acc, size) => acc + size, 0);
|
||||||
|
// console.log(`Result for ${file}:`, result);
|
||||||
|
results.push([
|
||||||
|
`${profile.name}`,
|
||||||
|
funcProfile,
|
||||||
|
"original",
|
||||||
|
file,
|
||||||
|
isPlain ? "plain" : "binary",
|
||||||
|
content.size.toString(),
|
||||||
|
result.chunkCount.toString(),
|
||||||
|
result.averageChunkSize.toFixed(2),
|
||||||
|
result.maxChunkSize.toString(),
|
||||||
|
result.minChunkSize.toString(),
|
||||||
|
result.uniqueChunks.toString(),
|
||||||
|
"",
|
||||||
|
savings.toString(),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
// add editions (inserting "*") to content on head, 5%, middle, 95%, tail to see if it affects the chunking
|
||||||
|
const editions = [
|
||||||
|
{ name: "head", content: modifyBlob(content, 0, "*") },
|
||||||
|
{ name: "5%", content: modifyBlob(content, Math.floor(content.size * 0.05), "*") },
|
||||||
|
{ name: "middle", content: modifyBlob(content, Math.floor(content.size * 0.5), "*") },
|
||||||
|
{ name: "95%", content: modifyBlob(content, Math.floor(content.size * 0.95), "*") },
|
||||||
|
{ name: "tail", content: modifyBlob(content, content.size, "*") },
|
||||||
|
];
|
||||||
|
const baseChunks = result.chunks;
|
||||||
|
for (const edition of editions) {
|
||||||
|
console.log(`Testing edition: ${edition.name}`);
|
||||||
|
const editionResult = await testSplit(fn, edition.content, profile.settings);
|
||||||
|
const sharedChunks = editionResult.chunks.filter((chunk) => baseChunks.includes(chunk)).length;
|
||||||
|
const newChunks = editionResult.chunks.filter((chunk) => !baseChunks.includes(chunk));
|
||||||
|
const editionResultChunkLength = editionResult.chunks.map((c) => c.length);
|
||||||
|
// console.log(`Result for edition ${edition.name} of ${file}:`, editionResult);
|
||||||
|
const editionSavings =
|
||||||
|
editionResult.originalSize - editionResultChunkLength.reduce((acc, size) => acc + size, 0);
|
||||||
|
// newly added chunks size :
|
||||||
|
const newChunksSize = newChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||||
|
results.push([
|
||||||
|
`${profile.name}`,
|
||||||
|
funcProfile,
|
||||||
|
`${edition.name}`,
|
||||||
|
file,
|
||||||
|
isPlain ? "plain" : "binary",
|
||||||
|
edition.content.size.toString(),
|
||||||
|
editionResult.chunkCount.toString(),
|
||||||
|
editionResult.averageChunkSize.toFixed(2),
|
||||||
|
editionResult.maxChunkSize.toString(),
|
||||||
|
editionResult.minChunkSize.toString(),
|
||||||
|
editionResult.uniqueChunks.toString(),
|
||||||
|
sharedChunks.toString(),
|
||||||
|
editionSavings.toString(),
|
||||||
|
newChunks.length.toString(),
|
||||||
|
newChunksSize.toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.unshift(header);
|
||||||
|
await fs.writeFile(resolve(__dirname, "splitResults.csv"), results.map((r) => r.join(",")).join("\n"));
|
||||||
|
}
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user