mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-14 11:31:17 +00:00
Compare commits
30 Commits
update_lib
...
0.25.61
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1859f5d2e | ||
|
|
785af8cb8f | ||
|
|
06e1f4aa4a | ||
|
|
767f22ce9c | ||
|
|
6a9bba702c | ||
|
|
de2397dc3f | ||
|
|
daaad9212e | ||
|
|
a6891374a1 | ||
|
|
b1cadf0549 | ||
|
|
95f40cc954 | ||
|
|
8deaf123d6 | ||
|
|
053813bffb | ||
|
|
cc7af03618 | ||
|
|
a130e3700e | ||
|
|
0549e901b2 | ||
|
|
e9afe06968 | ||
|
|
37715d4c9f | ||
|
|
106367fa41 | ||
|
|
538130aa91 | ||
|
|
c9d0357fec | ||
|
|
d05c76da36 | ||
|
|
d2eb6ecbaf | ||
|
|
25a6fde212 | ||
|
|
e8f8b680ef | ||
|
|
6c30f2b863 | ||
|
|
770d4af4a0 | ||
|
|
3b311248cb | ||
|
|
5772811a45 | ||
|
|
55529cd71e | ||
|
|
2e9b8b7b62 |
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import esbuild from "esbuild";
|
import esbuild from "esbuild";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import builtins from "builtin-modules";
|
|
||||||
import sveltePlugin from "esbuild-svelte";
|
import sveltePlugin from "esbuild-svelte";
|
||||||
import { sveltePreprocess } from "svelte-preprocess";
|
import { sveltePreprocess } from "svelte-preprocess";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|||||||
@@ -1,103 +1,83 @@
|
|||||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
||||||
import svelte from "eslint-plugin-svelte";
|
|
||||||
import _import from "eslint-plugin-import";
|
|
||||||
import { fixupPluginRules } from "@eslint/compat";
|
|
||||||
import tsParser from "@typescript-eslint/parser";
|
import tsParser from "@typescript-eslint/parser";
|
||||||
import path from "node:path";
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
import { fileURLToPath } from "node:url";
|
import globals from "globals";
|
||||||
import js from "@eslint/js";
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import * as sveltePlugin from "eslint-plugin-svelte";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
export default defineConfig([
|
||||||
const __dirname = path.dirname(__filename);
|
globalIgnores([
|
||||||
const compat = new FlatCompat({
|
"**/node_modules/*",
|
||||||
baseDirectory: __dirname,
|
"**/jest.config.js",
|
||||||
recommendedConfig: js.configs.recommended,
|
"src/lib/coverage",
|
||||||
allConfig: js.configs.all,
|
"src/lib/browsertest",
|
||||||
});
|
"**/test.ts",
|
||||||
|
"**/tests.ts",
|
||||||
export default [
|
"**/**test.ts",
|
||||||
|
"**/**.test.ts",
|
||||||
|
"**/*.unit.spec.ts",
|
||||||
|
"**/esbuild.*.mjs",
|
||||||
|
"**/terser.*.mjs",
|
||||||
|
"**/node_modules",
|
||||||
|
"**/build",
|
||||||
|
"**/.eslintrc.js.bak",
|
||||||
|
"src/lib/src/patches/pouchdb-utils",
|
||||||
|
"**/esbuild.config.mjs",
|
||||||
|
"**/rollup.config.js",
|
||||||
|
"modules/octagonal-wheels/rollup.config.js",
|
||||||
|
"modules/octagonal-wheels/dist/**/*",
|
||||||
|
"src/lib/test",
|
||||||
|
"src/lib/_tools",
|
||||||
|
"src/lib/src/cli",
|
||||||
|
"**/main.js",
|
||||||
|
"src/apps/**/*",
|
||||||
|
".prettierrc.*.mjs",
|
||||||
|
".prettierrc.mjs",
|
||||||
|
"*.config.mjs",
|
||||||
|
"src/apps/**/*",
|
||||||
|
"src/lib/src/services/implements/browser/**",
|
||||||
|
"src/lib/src/services/implements/headless/**",
|
||||||
|
"src/lib/src/API",
|
||||||
|
]),
|
||||||
|
...sveltePlugin.configs["flat/base"],
|
||||||
|
...obsidianmd.configs.recommended,
|
||||||
{
|
{
|
||||||
ignores: [
|
files: ["**/*.ts"],
|
||||||
"**/node_modules/*",
|
|
||||||
"**/jest.config.js",
|
|
||||||
"src/lib/coverage",
|
|
||||||
"src/lib/browsertest",
|
|
||||||
"**/test.ts",
|
|
||||||
"**/tests.ts",
|
|
||||||
"**/**test.ts",
|
|
||||||
"**/**.test.ts",
|
|
||||||
"**/esbuild.*.mjs",
|
|
||||||
"**/terser.*.mjs",
|
|
||||||
"**/node_modules",
|
|
||||||
"**/build",
|
|
||||||
"**/.eslintrc.js.bak",
|
|
||||||
"src/lib/src/patches/pouchdb-utils",
|
|
||||||
"**/esbuild.config.mjs",
|
|
||||||
"**/rollup.config.js",
|
|
||||||
"modules/octagonal-wheels/rollup.config.js",
|
|
||||||
"modules/octagonal-wheels/dist/**/*",
|
|
||||||
"src/lib/test",
|
|
||||||
"src/lib/_tools",
|
|
||||||
"src/lib/src/cli",
|
|
||||||
"**/main.js",
|
|
||||||
"src/apps/**/*",
|
|
||||||
".prettierrc.*.mjs",
|
|
||||||
".prettierrc.mjs",
|
|
||||||
"*.config.mjs"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...compat.extends(
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
),
|
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
"@typescript-eslint": typescriptEslint,
|
|
||||||
svelte,
|
|
||||||
import: fixupPluginRules(_import),
|
|
||||||
},
|
|
||||||
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser },
|
||||||
parser: tsParser,
|
parser: tsParser,
|
||||||
ecmaVersion: 5,
|
|
||||||
sourceType: "module",
|
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["tsconfig.json"],
|
project: "./tsconfig.json",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { args: "none" }],
|
||||||
"@typescript-eslint/no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
args: "none",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
"no-unused-labels": "off",
|
"no-unused-labels": "off",
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
"require-await": "error",
|
"require-await": "error",
|
||||||
|
"obsidianmd/rule-custom-message": "off", // Temporary
|
||||||
|
"obsidianmd/ui/sentence-case": "off", // Temporary
|
||||||
"@typescript-eslint/require-await": "warn",
|
"@typescript-eslint/require-await": "warn",
|
||||||
"@typescript-eslint/no-misused-promises": "warn",
|
"@typescript-eslint/no-misused-promises": "warn",
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"no-async-promise-executor": "warn",
|
"no-async-promise-executor": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
"no-constant-condition": ["error", { checkLoops: false }],
|
||||||
"no-constant-condition": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
checkLoops: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
files: ["**/*.svelte"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
|
||||||
|
"obsidianmd/no-plugin-as-component": "off", // Temporary
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.60",
|
"version": "0.25.61",
|
||||||
"minAppVersion": "0.9.12",
|
"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",
|
||||||
"authorUrl": "https://github.com/vrtmrz",
|
"authorUrl": "https://github.com/vrtmrz",
|
||||||
|
|||||||
1355
package-lock.json
generated
1355
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.60",
|
"version": "0.25.61",
|
||||||
"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",
|
||||||
@@ -61,8 +61,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
"@chialab/esbuild-plugin-worker": "^0.19.0",
|
||||||
"@eslint/compat": "^2.0.2",
|
|
||||||
"@eslint/eslintrc": "^3.3.4",
|
|
||||||
"@eslint/js": "^9.39.3",
|
"@eslint/js": "^9.39.3",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tsconfig/svelte": "^5.0.8",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
@@ -84,18 +82,15 @@
|
|||||||
"@vitest/browser": "^4.1.1",
|
"@vitest/browser": "^4.1.1",
|
||||||
"@vitest/browser-playwright": "^4.1.1",
|
"@vitest/browser-playwright": "^4.1.1",
|
||||||
"@vitest/coverage-v8": "^4.1.1",
|
"@vitest/coverage-v8": "^4.1.1",
|
||||||
"builtin-modules": "5.0.0",
|
|
||||||
"dotenv": "^17.3.1",
|
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.0",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"esbuild-svelte": "^0.9.4",
|
"esbuild-svelte": "^0.9.4",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||||
"eslint-plugin-svelte": "^3.15.0",
|
"eslint-plugin-svelte": "^3.15.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"glob": "^13.0.6",
|
"globals": "^14.0.0",
|
||||||
"obsidian": "^1.12.3",
|
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.58.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-load-config": "^6.0.1",
|
"postcss-load-config": "^6.0.1",
|
||||||
@@ -116,6 +111,7 @@
|
|||||||
"svelte-check": "^4.4.3",
|
"svelte-check": "^4.4.3",
|
||||||
"svelte-preprocess": "^6.0.3",
|
"svelte-preprocess": "^6.0.3",
|
||||||
"terser": "^5.39.0",
|
"terser": "^5.39.0",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
"transform-pouch": "^2.0.0",
|
"transform-pouch": "^2.0.0",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
@@ -136,6 +132,7 @@
|
|||||||
"@trystero-p2p/nostr": "^0.23.0",
|
"@trystero-p2p/nostr": "^0.23.0",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
|
"obsidian": "^1.12.3",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
import { LOG_LEVEL_INFO } from "octagonal-wheels/common/logger";
|
||||||
|
import type PouchDB from "pouchdb-core";
|
||||||
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
||||||
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
import type { HasSettings, ObsidianLiveSyncSettings, EntryDoc } from "./lib/src/common/types";
|
||||||
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
import { __$checkInstanceBinding } from "./lib/src/dev/checks";
|
||||||
@@ -123,7 +124,7 @@ export class LiveSyncBaseCore<
|
|||||||
for (const module of this.modules) {
|
for (const module of this.modules) {
|
||||||
if (module.constructor === constructor) return module as T;
|
if (module.constructor === constructor) return module as T;
|
||||||
}
|
}
|
||||||
throw new Error(`Module ${constructor} not found or not loaded.`);
|
throw new Error(`Module ${constructor.name} not found or not loaded.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,8 +161,10 @@ export class LiveSyncBaseCore<
|
|||||||
module.onBindFunction(this, this.services);
|
module.onBindFunction(this, this.services);
|
||||||
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
__$checkInstanceBinding(module); // Check if all functions are properly bound, and log warnings if not.
|
||||||
} else {
|
} else {
|
||||||
|
// module should not be never.
|
||||||
|
const moduleName = (module as unknown)?.constructor?.name ?? "unknown";
|
||||||
this.services.API.addLog(
|
this.services.API.addLog(
|
||||||
`Module ${(module as any)?.constructor?.name ?? "unknown"} does not have onBindFunction, skipping binding.`,
|
`Module ${moduleName} does not have onBindFunction, skipping binding.`,
|
||||||
LOG_LEVEL_INFO
|
LOG_LEVEL_INFO
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,12 +257,12 @@ describe("daemon command", () => {
|
|||||||
// failure 1: 30000*2=60000, failure 2: 30000*4=120000,
|
// failure 1: 30000*2=60000, failure 2: 30000*4=120000,
|
||||||
// failure 3: 30000*8=240000, failure 4: 30000*16=480000→capped, 5→cap, 6→cap
|
// failure 3: 30000*8=240000, failure 4: 30000*16=480000→capped, 5→cap, 6→cap
|
||||||
const expectedIntervals = [
|
const expectedIntervals = [
|
||||||
baseMs * 2, // after failure 1: 60000
|
baseMs * 2, // after failure 1: 60000
|
||||||
baseMs * 4, // after failure 2: 120000
|
baseMs * 4, // after failure 2: 120000
|
||||||
baseMs * 8, // after failure 3: 240000
|
baseMs * 8, // after failure 3: 240000
|
||||||
300_000, // after failure 4 (would be 480000, capped)
|
300_000, // after failure 4 (would be 480000, capped)
|
||||||
300_000, // after failure 5 (cap)
|
300_000, // after failure 5 (cap)
|
||||||
300_000, // after failure 6 (cap)
|
300_000, // after failure 6 (cap)
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const expected of expectedIntervals) {
|
for (const expected of expectedIntervals) {
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
|
|
||||||
// 3. Re-enable sync.
|
// 3. Re-enable sync.
|
||||||
const restoreSyncSettings = async () => {
|
const restoreSyncSettings = async () => {
|
||||||
await core.services.setting.applyPartial({
|
await core.services.setting.applyPartial(
|
||||||
...context.originalSyncSettings,
|
{
|
||||||
suspendFileWatching: false,
|
...context.originalSyncSettings,
|
||||||
}, true);
|
suspendFileWatching: false,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
// applySettings fires the full lifecycle: onSuspending → onResumed.
|
// applySettings fires the full lifecycle: onSuspending → onResumed.
|
||||||
// ModuleReplicatorCouchDB starts continuous replication on onResumed
|
// ModuleReplicatorCouchDB starts continuous replication on onResumed
|
||||||
// via fireAndForget.
|
// via fireAndForget.
|
||||||
@@ -54,10 +57,13 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
// Lifecycle events (onSuspending) may re-enable suspension flags.
|
// Lifecycle events (onSuspending) may re-enable suspension flags.
|
||||||
// Clear them explicitly after the lifecycle completes. applyPartial
|
// Clear them explicitly after the lifecycle completes. applyPartial
|
||||||
// with true is a direct store write — it does not re-trigger lifecycle.
|
// with true is a direct store write — it does not re-trigger lifecycle.
|
||||||
await core.services.setting.applyPartial({
|
await core.services.setting.applyPartial(
|
||||||
suspendFileWatching: false,
|
{
|
||||||
suspendParseReplicationResult: false,
|
suspendFileWatching: false,
|
||||||
}, true);
|
suspendParseReplicationResult: false,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
};
|
};
|
||||||
if (options.interval) {
|
if (options.interval) {
|
||||||
log(`Polling mode: syncing every ${options.interval}s`);
|
log(`Polling mode: syncing every ${options.interval}s`);
|
||||||
@@ -80,7 +86,9 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
currentIntervalMs = Math.min(baseIntervalMs * Math.pow(2, consecutiveFailures), maxIntervalMs);
|
currentIntervalMs = Math.min(baseIntervalMs * Math.pow(2, consecutiveFailures), maxIntervalMs);
|
||||||
console.error(`[Daemon] Poll error (${consecutiveFailures} consecutive):`, err);
|
console.error(`[Daemon] Poll error (${consecutiveFailures} consecutive):`, err);
|
||||||
if (consecutiveFailures >= 5) {
|
if (consecutiveFailures >= 5) {
|
||||||
console.error(`[Daemon] Warning: ${consecutiveFailures} consecutive failures, backing off to ${Math.round(currentIntervalMs / 1000)}s`);
|
console.error(
|
||||||
|
`[Daemon] Warning: ${consecutiveFailures} consecutive failures, backing off to ${Math.round(currentIntervalMs / 1000)}s`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollTimer = setTimeout(poll, currentIntervalMs);
|
pollTimer = setTimeout(poll, currentIntervalMs);
|
||||||
@@ -99,9 +107,11 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
|||||||
log("LiveSync active");
|
log("LiveSync active");
|
||||||
const currentSettings = core.services.setting.currentSettings();
|
const currentSettings = core.services.setting.currentSettings();
|
||||||
if (!currentSettings.liveSync && !currentSettings.syncOnStart) {
|
if (!currentSettings.liveSync && !currentSettings.syncOnStart) {
|
||||||
console.error("[Daemon] Warning: liveSync and syncOnStart are both disabled in settings. " +
|
console.error(
|
||||||
"No sync will occur. Set liveSync=true in your settings file for continuous sync, " +
|
"[Daemon] Warning: liveSync and syncOnStart are both disabled in settings. " +
|
||||||
"or use --interval for polling mode.");
|
"No sync will occur. Set liveSync=true in your settings file for continuous sync, " +
|
||||||
|
"or use --interval for polling mode."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,16 @@ export interface CLICommandContext {
|
|||||||
databasePath: string;
|
databasePath: string;
|
||||||
core: LiveSyncBaseCore<ServiceContext, any>;
|
core: LiveSyncBaseCore<ServiceContext, any>;
|
||||||
settingsPath: string;
|
settingsPath: string;
|
||||||
originalSyncSettings: Pick<ObsidianLiveSyncSettings, "liveSync" | "syncOnStart" | "periodicReplication" | "syncOnSave" | "syncOnEditorSave" | "syncOnFileOpen" | "syncAfterMerge">;
|
originalSyncSettings: Pick<
|
||||||
|
ObsidianLiveSyncSettings,
|
||||||
|
| "liveSync"
|
||||||
|
| "syncOnStart"
|
||||||
|
| "periodicReplication"
|
||||||
|
| "syncOnSave"
|
||||||
|
| "syncOnEditorSave"
|
||||||
|
| "syncOnFileOpen"
|
||||||
|
| "syncAfterMerge"
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VALID_COMMANDS = new Set([
|
export const VALID_COMMANDS = new Set([
|
||||||
|
|||||||
@@ -280,16 +280,13 @@ export async function main() {
|
|||||||
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
// chokidar's ignored option is populated when beginWatch() fires during onLoad().
|
||||||
const watchEnabled = options.command === "daemon";
|
const watchEnabled = options.command === "daemon";
|
||||||
const vaultPath =
|
const vaultPath =
|
||||||
options.command === "mirror" && options.commandArgs[0]
|
options.command === "mirror" && options.commandArgs[0] ? path.resolve(options.commandArgs[0]) : databasePath;
|
||||||
? path.resolve(options.commandArgs[0])
|
|
||||||
: databasePath;
|
|
||||||
let ignoreRules: IgnoreRules | undefined;
|
let ignoreRules: IgnoreRules | undefined;
|
||||||
if (options.command === "daemon" || options.command === "mirror") {
|
if (options.command === "daemon" || options.command === "mirror") {
|
||||||
ignoreRules = new IgnoreRules(vaultPath);
|
ignoreRules = new IgnoreRules(vaultPath);
|
||||||
await ignoreRules.load();
|
await ignoreRules.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create service context and hub
|
// Create service context and hub
|
||||||
const context = new NodeServiceContext(databasePath);
|
const context = new NodeServiceContext(databasePath);
|
||||||
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
const serviceHubInstance = new NodeServiceHub<NodeServiceContext>(databasePath, context);
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ class CLIConverterAdapter implements IStorageEventConverterAdapter<NodeFile> {
|
|||||||
class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
class CLIWatchAdapter implements IStorageEventWatchAdapter {
|
||||||
private _watcher: FSWatcher | undefined;
|
private _watcher: FSWatcher | undefined;
|
||||||
|
|
||||||
constructor(private basePath: string, private ignoreRules?: IgnoreRules, private watchEnabled: boolean = false) {}
|
constructor(
|
||||||
|
private basePath: string,
|
||||||
|
private ignoreRules?: IgnoreRules,
|
||||||
|
private watchEnabled: boolean = false
|
||||||
|
) {}
|
||||||
|
|
||||||
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -60,10 +60,7 @@ describe("CLIStorageEventManagerAdapter", () => {
|
|||||||
await adapter.watch.beginWatch(handlers);
|
await adapter.watch.beginWatch(handlers);
|
||||||
|
|
||||||
expect(chokidar.watch).toHaveBeenCalledTimes(1);
|
expect(chokidar.watch).toHaveBeenCalledTimes(1);
|
||||||
expect(chokidar.watch).toHaveBeenCalledWith(
|
expect(chokidar.watch).toHaveBeenCalledWith("/base", expect.objectContaining({ ignoreInitial: true }));
|
||||||
"/base",
|
|
||||||
expect.objectContaining({ ignoreInitial: true })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("add event produces NodeFile with correct relative path via onCreate", async () => {
|
it("add event produces NodeFile with correct relative path via onCreate", async () => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function initialiseServiceModulesCLI(
|
|||||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||||
services: InjectableServiceHub<ServiceContext>,
|
services: InjectableServiceHub<ServiceContext>,
|
||||||
ignoreRules?: IgnoreRules,
|
ignoreRules?: IgnoreRules,
|
||||||
watchEnabled: boolean = false,
|
watchEnabled: boolean = false
|
||||||
): ServiceModules {
|
): ServiceModules {
|
||||||
const storageAccessManager = new StorageAccessManager();
|
const storageAccessManager = new StorageAccessManager();
|
||||||
|
|
||||||
@@ -39,13 +39,19 @@ export function initialiseServiceModulesCLI(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// CLI-specific storage event manager
|
// CLI-specific storage event manager
|
||||||
const storageEventManager = new StorageEventManagerCLI(basePath, core, {
|
const storageEventManager = new StorageEventManagerCLI(
|
||||||
fileProcessing: services.fileProcessing,
|
basePath,
|
||||||
setting: services.setting,
|
core,
|
||||||
vaultService: services.vault,
|
{
|
||||||
storageAccessManager: storageAccessManager,
|
fileProcessing: services.fileProcessing,
|
||||||
APIService: services.API,
|
setting: services.setting,
|
||||||
}, ignoreRules, watchEnabled);
|
vaultService: services.vault,
|
||||||
|
storageAccessManager: storageAccessManager,
|
||||||
|
APIService: services.API,
|
||||||
|
},
|
||||||
|
ignoreRules,
|
||||||
|
watchEnabled
|
||||||
|
);
|
||||||
|
|
||||||
// Close the file watcher during graceful shutdown so the process can exit cleanly.
|
// Close the file watcher during graceful shutdown so the process can exit cleanly.
|
||||||
services.appLifecycle.onUnload.addHandler(async () => {
|
services.appLifecycle.onUnload.addHandler(async () => {
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export class IgnoreRules {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (trimmed.startsWith("import:")) {
|
if (trimmed.startsWith("import:")) {
|
||||||
console.error(`[IgnoreRules] Warning: unrecognised directive '${trimmed}' — only 'import: .gitignore' is supported`);
|
console.error(
|
||||||
|
`[IgnoreRules] Warning: unrecognised directive '${trimmed}' — only 'import: .gitignore' is supported`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this._addPattern(trimmed);
|
this._addPattern(trimmed);
|
||||||
@@ -105,7 +107,7 @@ export class IgnoreRules {
|
|||||||
if (raw.startsWith("!")) {
|
if (raw.startsWith("!")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[IgnoreRules] Negation pattern '${raw}' is not supported. ` +
|
`[IgnoreRules] Negation pattern '${raw}' is not supported. ` +
|
||||||
`Remove it from .livesync/ignore or use a separate include/exclude file.`
|
`Remove it from .livesync/ignore or use a separate include/exclude file.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.patterns.push(this._normalisePattern(raw));
|
this.patterns.push(this._normalisePattern(raw));
|
||||||
|
|||||||
@@ -122,10 +122,7 @@ describe("IgnoreRules", () => {
|
|||||||
describe("load() with comments and blank lines", () => {
|
describe("load() with comments and blank lines", () => {
|
||||||
it("skips # comment lines and blank lines", async () => {
|
it("skips # comment lines and blank lines", async () => {
|
||||||
const vaultPath = await createVault();
|
const vaultPath = await createVault();
|
||||||
await writeIgnoreFile(
|
await writeIgnoreFile(vaultPath, "# This is a comment\n\n \n*.tmp\n# another comment\nbuild/\n");
|
||||||
vaultPath,
|
|
||||||
"# This is a comment\n\n \n*.tmp\n# another comment\nbuild/\n"
|
|
||||||
);
|
|
||||||
const rules = new IgnoreRules(vaultPath);
|
const rules = new IgnoreRules(vaultPath);
|
||||||
await rules.load();
|
await rules.load();
|
||||||
expect(rules.shouldIgnore("scratch.tmp")).toBe(true);
|
expect(rules.shouldIgnore("scratch.tmp")).toBe(true);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export async function runScenario(remoteType: RemoteType, encrypt: boolean): Pro
|
|||||||
const dbPrefix = remoteType === "COUCHDB" ? requireEnv("COUCHDB_DBNAME", "dbname") : "";
|
const dbPrefix = remoteType === "COUCHDB" ? requireEnv("COUCHDB_DBNAME", "dbname") : "";
|
||||||
const dbname = remoteType === "COUCHDB" ? `${dbPrefix}-${dbSuffix}` : "";
|
const dbname = remoteType === "COUCHDB" ? `${dbPrefix}-${dbSuffix}` : "";
|
||||||
|
|
||||||
const minioEndpoint = remoteType === "MINIO" ? requireEnv("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
|
const minioEndpoint =
|
||||||
|
remoteType === "MINIO" ? requireEnv("MINIO_ENDPOINT", "minioEndpoint").replace(/\/$/, "") : "";
|
||||||
const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
|
const minioAccessKey = remoteType === "MINIO" ? requireEnv("MINIO_ACCESS_KEY", "accessKey") : "";
|
||||||
const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
|
const minioSecretKey = remoteType === "MINIO" ? requireEnv("MINIO_SECRET_KEY", "secretKey") : "";
|
||||||
const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
|
const minioBucketBase = remoteType === "MINIO" ? requireEnv("MINIO_BUCKET_NAME", "bucketName") : "";
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ function injectBanner(): import("vite").Plugin {
|
|||||||
// Insert after the shebang line if present, otherwise at the top.
|
// Insert after the shebang line if present, otherwise at the top.
|
||||||
if (chunk.code.startsWith("#!")) {
|
if (chunk.code.startsWith("#!")) {
|
||||||
const newline = chunk.code.indexOf("\n");
|
const newline = chunk.code.indexOf("\n");
|
||||||
chunk.code = chunk.code.slice(0, newline + 1) + fileReaderPolyfillBanner + chunk.code.slice(newline + 1);
|
chunk.code =
|
||||||
|
chunk.code.slice(0, newline + 1) + fileReaderPolyfillBanner + chunk.code.slice(newline + 1);
|
||||||
} else {
|
} else {
|
||||||
chunk.code = fileReaderPolyfillBanner + chunk.code;
|
chunk.code = fileReaderPolyfillBanner + chunk.code;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
|
|||||||
|
|
||||||
const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]);
|
const [items, lastUsedId] = await Promise.all([historyStore.getVaultHistory(), historyStore.getLastUsedVaultId()]);
|
||||||
|
|
||||||
listEl.innerHTML = "";
|
listEl.replaceChildren();
|
||||||
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
emptyEl.classList.toggle("is-hidden", items.length > 0);
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|||||||
2
src/lib
2
src/lib
Submodule src/lib updated: adcfe42522...f6a6c2dff0
@@ -1,5 +1,6 @@
|
|||||||
import { Notice, Plugin, type App, type PluginManifest } from "./deps";
|
import { getLanguage, Notice, Plugin, type App, type PluginManifest } from "./deps";
|
||||||
|
import { setGetLanguage } from "./lib/src/common/coreEnvFunctions.ts";
|
||||||
|
setGetLanguage(getLanguage);
|
||||||
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
import { LiveSyncCommands } from "./features/LiveSyncCommands.ts";
|
||||||
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "./features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
import { ConfigSync } from "./features/ConfigSync/CmdConfigSync.ts";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
import { TFile, Modal, App, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from "../../../deps.ts";
|
||||||
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
import { getPathFromTFile, isValidPath } from "../../../common/utils.ts";
|
||||||
import { decodeBinary, escapeStringToHTML, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
import { decodeBinary, readString } from "../../../lib/src/string_and_binary/convert.ts";
|
||||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||||
import {
|
import {
|
||||||
type DocumentID,
|
type DocumentID,
|
||||||
@@ -145,22 +145,66 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareContentView(usePreformatted = true) {
|
||||||
|
this.contentView.empty();
|
||||||
|
this.contentView.toggleClass("op-pre", usePreformatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTextDiff(diff: [number, string][]) {
|
||||||
|
for (const [operation, text] of diff) {
|
||||||
|
if (operation == DIFF_DELETE) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-deleted" });
|
||||||
|
} else if (operation == DIFF_EQUAL) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-normal" });
|
||||||
|
} else if (operation == DIFF_INSERT) {
|
||||||
|
this.contentView.createSpan({ text, cls: "history-added" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendImageDiff(baseSrc: string, overlaySrc?: string) {
|
||||||
|
const wrap = this.contentView.createDiv({ cls: "ls-imgdiff-wrap" });
|
||||||
|
const overlay = wrap.createDiv({ cls: "overlay" });
|
||||||
|
overlay.createEl("img", { cls: "img-base" }, (img) => {
|
||||||
|
img.src = baseSrc;
|
||||||
|
});
|
||||||
|
if (overlaySrc) {
|
||||||
|
overlay.createEl("img", { cls: "img-overlay" }, (img) => {
|
||||||
|
img.src = overlaySrc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendDeletedNotice(usePreformatted = true) {
|
||||||
|
const notice = "(At this revision, the file has been deleted)";
|
||||||
|
if (usePreformatted) {
|
||||||
|
this.contentView.appendText(`${notice}\n`);
|
||||||
|
} else {
|
||||||
|
this.contentView.createDiv({ text: notice });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async showExactRev(rev: string) {
|
async showExactRev(rev: string) {
|
||||||
const db = this.core.localDatabase;
|
const db = this.core.localDatabase;
|
||||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||||
this.currentText = "";
|
this.currentText = "";
|
||||||
this.currentDeleted = false;
|
this.currentDeleted = false;
|
||||||
|
this.prepareContentView();
|
||||||
if (w === false) {
|
if (w === false) {
|
||||||
this.currentDeleted = true;
|
this.currentDeleted = true;
|
||||||
this.info.innerHTML = "";
|
this.info.empty();
|
||||||
this.contentView.innerHTML = `Could not read this revision<br>(${rev})`;
|
this.contentView.appendText("Could not read this revision");
|
||||||
|
this.contentView.createEl("br");
|
||||||
|
this.contentView.appendText(`(${rev})`);
|
||||||
} else {
|
} else {
|
||||||
this.currentDoc = w;
|
this.currentDoc = w;
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.setText(`Modified:${new Date(w.mtime).toLocaleString()}`);
|
||||||
let result = undefined;
|
|
||||||
const w1data = readDocument(w);
|
const w1data = readDocument(w);
|
||||||
this.currentDeleted = !!w.deleted;
|
this.currentDeleted = !!w.deleted;
|
||||||
// this.currentText = w1data;
|
if (typeof w1data == "string") {
|
||||||
|
this.currentText = w1data;
|
||||||
|
}
|
||||||
|
let rendered = false;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||||
@@ -168,58 +212,55 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||||
if (w2 != false) {
|
if (w2 != false) {
|
||||||
if (typeof w1data == "string") {
|
if (typeof w1data == "string") {
|
||||||
result = "";
|
const w2data = readDocument(w2);
|
||||||
const dmp = new diff_match_patch();
|
if (typeof w2data == "string") {
|
||||||
const w2data = readDocument(w2) as string;
|
const dmp = new diff_match_patch();
|
||||||
const diff = dmp.diff_main(w2data, w1data);
|
const diff = dmp.diff_main(w2data, w1data);
|
||||||
dmp.diff_cleanupSemantic(diff);
|
dmp.diff_cleanupSemantic(diff);
|
||||||
for (const v of diff) {
|
if (this.currentDeleted) {
|
||||||
const x1 = v[0];
|
this.appendDeletedNotice();
|
||||||
const x2 = v[1];
|
|
||||||
if (x1 == DIFF_DELETE) {
|
|
||||||
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
|
||||||
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
} else if (x1 == DIFF_INSERT) {
|
|
||||||
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
|
||||||
}
|
}
|
||||||
|
this.appendTextDiff(diff);
|
||||||
|
rendered = true;
|
||||||
}
|
}
|
||||||
result = result.replace(/\n/g, "<br>");
|
|
||||||
} else if (isImage(this.file)) {
|
} else if (isImage(this.file)) {
|
||||||
const src = this.generateBlobURL("base", w1data);
|
const src = this.generateBlobURL("base", w1data);
|
||||||
const overlay = this.generateBlobURL(
|
const overlay = this.generateBlobURL(
|
||||||
"overlay",
|
"overlay",
|
||||||
readDocument(w2) as Uint8Array<ArrayBuffer>
|
readDocument(w2) as Uint8Array<ArrayBuffer>
|
||||||
);
|
);
|
||||||
result = `<div class='ls-imgdiff-wrap'>
|
this.prepareContentView(false);
|
||||||
<div class='overlay'>
|
if (this.currentDeleted) {
|
||||||
<img class='img-base' src="${src}">
|
this.appendDeletedNotice(false);
|
||||||
<img class='img-overlay' src='${overlay}'>
|
}
|
||||||
</div>
|
this.appendImageDiff(src, overlay);
|
||||||
</div>`;
|
rendered = true;
|
||||||
this.contentView.removeClass("op-pre");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == undefined) {
|
if (!rendered) {
|
||||||
if (typeof w1data != "string") {
|
if (typeof w1data != "string") {
|
||||||
if (isImage(this.file)) {
|
if (isImage(this.file)) {
|
||||||
const src = this.generateBlobURL("base", w1data);
|
const src = this.generateBlobURL("base", w1data);
|
||||||
result = `<div class='ls-imgdiff-wrap'>
|
this.prepareContentView(false);
|
||||||
<div class='overlay'>
|
if (this.currentDeleted) {
|
||||||
<img class='img-base' src="${src}">
|
this.appendDeletedNotice(false);
|
||||||
</div>
|
}
|
||||||
</div>`;
|
this.appendImageDiff(src);
|
||||||
this.contentView.removeClass("op-pre");
|
} else {
|
||||||
|
if (this.currentDeleted) {
|
||||||
|
this.appendDeletedNotice();
|
||||||
|
}
|
||||||
|
this.contentView.appendText("Binary file");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = escapeStringToHTML(w1data);
|
if (this.currentDeleted) {
|
||||||
|
this.appendDeletedNotice();
|
||||||
|
}
|
||||||
|
this.contentView.appendText(w1data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
|
|
||||||
this.contentView.innerHTML =
|
|
||||||
(this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
|
||||||
}
|
}
|
||||||
// Reset diff navigation after content changes
|
// Reset diff navigation after content changes
|
||||||
this.resetDiffNavigation();
|
this.resetDiffNavigation();
|
||||||
@@ -245,8 +286,7 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
if (direction === "next") {
|
if (direction === "next") {
|
||||||
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
this.currentDiffIndex = (this.currentDiffIndex + 1) % diffElements.length;
|
||||||
} else {
|
} else {
|
||||||
this.currentDiffIndex =
|
this.currentDiffIndex = this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
||||||
this.currentDiffIndex <= 0 ? diffElements.length - 1 : this.currentDiffIndex - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = diffElements[this.currentDiffIndex];
|
const target = diffElements[this.currentDiffIndex];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { App, Modal } from "../../../deps.ts";
|
import { App, Modal } from "../../../deps.ts";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
|
import { CANCELLED, LEAVE_TO_SUBSEQUENT, type diff_result } from "../../../lib/src/common/types.ts";
|
||||||
import { escapeStringToHTML } from "../../../lib/src/string_and_binary/convert.ts";
|
|
||||||
import { delay } from "../../../lib/src/common/utils.ts";
|
import { delay } from "../../../lib/src/common/utils.ts";
|
||||||
import { eventHub } from "../../../common/events.ts";
|
import { eventHub } from "../../../common/events.ts";
|
||||||
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
|
import { globalSlipBoard } from "../../../lib/src/bureau/bureau.ts";
|
||||||
@@ -44,6 +43,25 @@ export class ConflictResolveModal extends Modal {
|
|||||||
// sendValue("close-resolve-conflict:" + this.filename, false);
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendDiffFragment(container: HTMLDivElement, text: string, cls: string) {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const span = container.createSpan({ cls });
|
||||||
|
span.textContent = line;
|
||||||
|
if (index < lines.length - 1) {
|
||||||
|
container.createSpan({ cls: "ls-mark-cr" });
|
||||||
|
container.createEl("br");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appendVersionInfo(container: HTMLDivElement, cls: string, name: string, date: string) {
|
||||||
|
const line = container.createSpan({ cls });
|
||||||
|
line.createSpan({ text: name, cls: "conflict-dev-name" });
|
||||||
|
line.appendText(`: ${date}`);
|
||||||
|
container.createEl("br");
|
||||||
|
}
|
||||||
|
|
||||||
override onOpen() {
|
override onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
// Send cancel signal for the previous merge dialogue
|
// Send cancel signal for the previous merge dialogue
|
||||||
@@ -64,25 +82,21 @@ export class ConflictResolveModal extends Modal {
|
|||||||
const div = contentEl.createDiv("");
|
const div = contentEl.createDiv("");
|
||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
div.addClass("ls-dialog");
|
div.addClass("ls-dialog");
|
||||||
let diff = "";
|
let diffLength = 0;
|
||||||
for (const v of this.result.diff) {
|
for (const v of this.result.diff) {
|
||||||
const x1 = v[0];
|
const x1 = v[0];
|
||||||
const x2 = v[1];
|
const x2 = v[1];
|
||||||
|
diffLength += x2.length;
|
||||||
|
if (diffLength > 100 * 1024) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (x1 == DIFF_DELETE) {
|
if (x1 == DIFF_DELETE) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "deleted");
|
||||||
"<span class='deleted'>" +
|
div.createEl("span", { text: x2, cls: "deleted normal conflict-dev-name" });
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
} else if (x1 == DIFF_EQUAL) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "normal");
|
||||||
"<span class='normal'>" +
|
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
} else if (x1 == DIFF_INSERT) {
|
} else if (x1 == DIFF_INSERT) {
|
||||||
diff +=
|
this.appendDiffFragment(div, x2, "added");
|
||||||
"<span class='added'>" +
|
|
||||||
escapeStringToHTML(x2).replace(/\n/g, "<span class='ls-mark-cr'></span>\n") +
|
|
||||||
"</span>";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +106,8 @@ export class ConflictResolveModal extends Modal {
|
|||||||
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
new Date(this.result.left.mtime).toLocaleString() + (this.result.left.deleted ? " (Deleted)" : "");
|
||||||
const date2 =
|
const date2 =
|
||||||
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
new Date(this.result.right.mtime).toLocaleString() + (this.result.right.deleted ? " (Deleted)" : "");
|
||||||
div2.innerHTML = `<span class='deleted'><span class='conflict-dev-name'>${this.localName}</span>: ${date1}</span><br>
|
this.appendVersionInfo(div2, "deleted", this.localName, date1);
|
||||||
<span class='added'><span class='conflict-dev-name'>${this.remoteName}</span>: ${date2}</span><br>`;
|
this.appendVersionInfo(div2, "added", this.remoteName, date2);
|
||||||
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
contentEl.createEl("button", { text: `Use ${this.localName}` }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
e.addEventListener("click", () => this.sendResponse(this.result.right.rev))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
@@ -108,11 +122,9 @@ export class ConflictResolveModal extends Modal {
|
|||||||
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
contentEl.createEl("button", { text: !this.pluginPickMode ? "Not now" : "Cancel" }, (e) =>
|
||||||
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
e.addEventListener("click", () => this.sendResponse(CANCELLED))
|
||||||
).style.marginRight = "4px";
|
).style.marginRight = "4px";
|
||||||
diff = diff.replace(/\n/g, "<br>");
|
if (diffLength > 100 * 1024) {
|
||||||
if (diff.length > 100 * 1024) {
|
div.empty();
|
||||||
div.innerText = "(Too large diff to display)";
|
div.innerText = "(Too large diff to display)";
|
||||||
} else {
|
|
||||||
div.innerHTML = diff;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,13 @@ export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElem
|
|||||||
// tmpDiv.addClass("sls-header-button");
|
// tmpDiv.addClass("sls-header-button");
|
||||||
tmpDiv.addClass("op-warn-info");
|
tmpDiv.addClass("op-warn-info");
|
||||||
|
|
||||||
tmpDiv.innerHTML = `<p>${$msg("obsidianLiveSyncSettingTab.msgNewVersionNote")}</p><button>${$msg("obsidianLiveSyncSettingTab.optionOkReadEverything")}</button>`;
|
tmpDiv.createEl("p", { text: $msg("obsidianLiveSyncSettingTab.msgNewVersionNote") });
|
||||||
|
const readEverythingButton = tmpDiv.createEl("button", {
|
||||||
|
text: $msg("obsidianLiveSyncSettingTab.optionOkReadEverything"),
|
||||||
|
});
|
||||||
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
|
if (lastVersion > (this.editingSettings?.lastReadUpdates || 0)) {
|
||||||
const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
|
const informationButtonDiv = informationDivEl.appendChild(tmpDiv);
|
||||||
informationButtonDiv.querySelector("button")?.addEventListener("click", () => {
|
readEverythingButton.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
this.editingSettings.lastReadUpdates = lastVersion;
|
this.editingSettings.lastReadUpdates = lastVersion;
|
||||||
await this.saveAllDirtySettings();
|
await this.saveAllDirtySettings();
|
||||||
|
|||||||
@@ -121,13 +121,13 @@ export function paneSetup(
|
|||||||
const repo = "vrtmrz/obsidian-livesync";
|
const repo = "vrtmrz/obsidian-livesync";
|
||||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||||
this.createEl(
|
this.createEl(paneEl, "div", "", (el) => {
|
||||||
paneEl,
|
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||||
"div",
|
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||||
"",
|
anchor.target = "_blank";
|
||||||
(el) =>
|
anchor.rel = "noopener";
|
||||||
(el.innerHTML = `<a href='https://github.com/${repo}/blob/main${topPath}' target="_blank">${$msg("obsidianLiveSyncSettingTab.linkOpenInBrowser")}</a>`)
|
});
|
||||||
);
|
});
|
||||||
const troubleShootEl = this.createEl(paneEl, "div", {
|
const troubleShootEl = this.createEl(paneEl, "div", {
|
||||||
text: "",
|
text: "",
|
||||||
cls: "sls-troubleshoot-preview",
|
cls: "sls-troubleshoot-preview",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const checkConfig = async (
|
|||||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckingDbConfig"), LOG_LEVEL_INFO);
|
||||||
let isSuccessful = true;
|
let isSuccessful = true;
|
||||||
const emptyDiv = createDiv();
|
const emptyDiv = createDiv();
|
||||||
emptyDiv.innerHTML = "<span></span>";
|
emptyDiv.createSpan();
|
||||||
checkResultDiv?.replaceChildren(...[emptyDiv]);
|
checkResultDiv?.replaceChildren(...[emptyDiv]);
|
||||||
const addResult = (msg: string, classes?: string[]) => {
|
const addResult = (msg: string, classes?: string[]) => {
|
||||||
const tmpDiv = createDiv();
|
const tmpDiv = createDiv();
|
||||||
@@ -21,7 +21,7 @@ export const checkConfig = async (
|
|||||||
if (classes) {
|
if (classes) {
|
||||||
tmpDiv.addClasses(classes);
|
tmpDiv.addClasses(classes);
|
||||||
}
|
}
|
||||||
tmpDiv.innerHTML = `${msg}`;
|
tmpDiv.textContent = msg;
|
||||||
checkResultDiv?.appendChild(tmpDiv);
|
checkResultDiv?.appendChild(tmpDiv);
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -47,9 +47,10 @@ export const checkConfig = async (
|
|||||||
if (!checkResultDiv) return;
|
if (!checkResultDiv) return;
|
||||||
const tmpDiv = createDiv();
|
const tmpDiv = createDiv();
|
||||||
tmpDiv.addClass("ob-btn-config-fix");
|
tmpDiv.addClass("ob-btn-config-fix");
|
||||||
tmpDiv.innerHTML = `<label>${title}</label><button>${$msg("obsidianLiveSyncSettingTab.btnFix")}</button>`;
|
tmpDiv.createEl("label", { text: title });
|
||||||
|
const fixButton = tmpDiv.createEl("button", { text: $msg("obsidianLiveSyncSettingTab.btnFix") });
|
||||||
const x = checkResultDiv.appendChild(tmpDiv);
|
const x = checkResultDiv.appendChild(tmpDiv);
|
||||||
x.querySelector("button")?.addEventListener("click", () => {
|
fixButton.addEventListener("click", () => {
|
||||||
fireAndForget(async () => {
|
fireAndForget(async () => {
|
||||||
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
|
Logger($msg("obsidianLiveSyncSettingTab.logCouchDbConfigSet", { title, key, value }));
|
||||||
const res = await requestToCouchDBWithCredentials(
|
const res = await requestToCouchDBWithCredentials(
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
import Decision from "@/lib/src/UI/components/Decision.svelte";
|
||||||
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
import Instruction from "@/lib/src/UI/components/Instruction.svelte";
|
||||||
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
import UserDecisions from "@/lib/src/UI/components/UserDecisions.svelte";
|
||||||
const TYPE_CLOSE = "close";
|
const TYPE_CLOSE = "close";
|
||||||
type ResultType = typeof TYPE_CLOSE;
|
type ResultType = typeof TYPE_CLOSE;
|
||||||
type Props = {
|
type Props = {
|
||||||
setResult: (result: ResultType) => void;
|
setResult: (_result: ResultType) => void;
|
||||||
};
|
};
|
||||||
const { setResult }: Props = $props();
|
const { setResult }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,11 +3,22 @@ import { createServiceFeature } from "@lib/interfaces/ServiceModule";
|
|||||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
|
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@lib/common/rosetta";
|
||||||
import { $msg, setLang } from "@lib/common/i18n";
|
import { $msg, setLang } from "@lib/common/i18n";
|
||||||
|
|
||||||
|
function tryGetLanguage() {
|
||||||
|
try {
|
||||||
|
// Note: 1.8.7+ is required. but it is 18, Feb., 2025. we want to fallback on earlier versions, so we catch the error here.
|
||||||
|
// eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
return getLanguage();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to get Obsidian language, defaulting to 'def'", e);
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
|
export const enableI18nFeature = createServiceFeature(async ({ services: { setting, API } }) => {
|
||||||
let isChanged = false;
|
let isChanged = false;
|
||||||
const settings = setting.currentSettings();
|
const settings = setting.currentSettings();
|
||||||
if (settings.displayLanguage == "") {
|
if (settings.displayLanguage == "") {
|
||||||
const obsidianLanguage = getLanguage();
|
const obsidianLanguage = tryGetLanguage();
|
||||||
if (
|
if (
|
||||||
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
SUPPORTED_I18N_LANGS.indexOf(obsidianLanguage) !== -1 && // Check if the language is supported
|
||||||
obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting
|
obsidianLanguage != settings.displayLanguage // Check if the language is different from the current setting
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type UseP2PReplicatorResult } from "@/lib/src/replication/trystero/UseP
|
|||||||
import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector";
|
import { P2PLogCollector } from "@/lib/src/replication/trystero/P2PLogCollector";
|
||||||
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView";
|
import { P2PReplicatorPaneView, VIEW_TYPE_P2P } from "@/features/P2PSync/P2PReplicator/P2PReplicatorPaneView";
|
||||||
import type { LiveSyncCore } from "@/main";
|
import type { LiveSyncCore } from "@/main";
|
||||||
|
import type { WorkspaceLeaf } from "@/deps";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServiceFeature: P2P Replicator lifecycle management.
|
* ServiceFeature: P2P Replicator lifecycle management.
|
||||||
@@ -43,7 +44,7 @@ export function useP2PReplicatorUI(
|
|||||||
|
|
||||||
// Register view, commands and ribbon if a view factory is provided
|
// Register view, commands and ribbon if a view factory is provided
|
||||||
const viewType = VIEW_TYPE_P2P;
|
const viewType = VIEW_TYPE_P2P;
|
||||||
const factory = (leaf: any) => {
|
const factory = (leaf: WorkspaceLeaf) => {
|
||||||
return new P2PReplicatorPaneView(leaf, core, {
|
return new P2PReplicatorPaneView(leaf, core, {
|
||||||
replicator: getReplicator(),
|
replicator: getReplicator(),
|
||||||
p2pLogCollector,
|
p2pLogCollector,
|
||||||
|
|||||||
@@ -38,10 +38,20 @@ export class ObsidianVaultAdapter implements IVaultAdapter<TFile> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
async delete(file: TFile | TFolder, force = false): Promise<void> {
|
||||||
|
// if ("trashFile" in this.app.fileManager) {
|
||||||
|
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
// return await this.app.fileManager.trashFile(file);
|
||||||
|
// }
|
||||||
|
//TODO: need fix
|
||||||
return await this.app.vault.delete(file, force);
|
return await this.app.vault.delete(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
async trash(file: TFile | TFolder, force = false): Promise<void> {
|
||||||
|
// if ("trashFile" in this.app.fileManager) {
|
||||||
|
// // eslint-disable-next-line obsidianmd/no-unsupported-api
|
||||||
|
// return await this.app.fileManager.trashFile(file);
|
||||||
|
// }
|
||||||
|
//TODO: need fix
|
||||||
return await this.app.vault.trash(file, force);
|
return await this.app.vault.trash(file, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
updates.md
33
updates.md
@@ -3,7 +3,15 @@ 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.61
|
||||||
|
|
||||||
|
13th May, 2026
|
||||||
|
|
||||||
|
Reviews have started on the Obsidian Community, haven't they? It was quite a struggle, what with having to fix the outdated ESLint.
|
||||||
|
I am a bit nervous, but it is far better than just plodding along aimlessly, so let us get on with it. If you spot any issues, please let me know straight away.
|
||||||
|
|
||||||
|
From now on, I am avoiding committing directly to the main branch. This is because you lots have all been sending so much PRs. I wanted to keep things harmonious.
|
||||||
|
That said, I am still not used to rebasing, so there are some parts where the commit history is a right mess. I will work on improving that.
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
|
|
||||||
@@ -14,6 +22,29 @@ This P2P synchronisation is not compatible with previous versions in terms of co
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- No longer baffling errors occur when setting-update is triggered during the early stage of initialisation.
|
- No longer baffling errors occur when setting-update is triggered during the early stage of initialisation.
|
||||||
|
- Network error notice pop-ups are now suppressed when 'NetworkWarningStyle' is set to 'Hidden'. (Thank you so much @SeleiXi!)
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
- Diff navigation buttons have been added to the diff view, making it easier to move between differences. (Thank you so much @SeleiXi! #871)
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
- Chinese (Simplified) translations for settings and the Setup Wizard have been added. (Thank you so much @zombiek731!)
|
||||||
|
- Common UI controls and signal words are now localised into Chinese (Simplified). (Thank you so much @zombiek731!)
|
||||||
|
- i18n runtime behaviour and locale coverage have been improved. (Thank you so much @52sanmao!)
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
#### New features
|
||||||
|
|
||||||
|
- Daemon synchronisation is now supported. (Thank you so much @andrewleech! #843)
|
||||||
|
- `HeadlessConfirm` has been implemented with sensible defaults, enabling unattended operation in headless environments. (Thank you so much @andrewleech!)
|
||||||
|
- The CLI onboarding experience has been improved. (Thank you so much @OriBoharon! #872)
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Sub-millisecond CLI mtimes are now truncated to prevent mobile crash. (Thank you so much @brian-spackman! #893)
|
||||||
|
|
||||||
## 0.25.60
|
## 0.25.60
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"0.25.61": "1.7.2",
|
||||||
|
"0.25.60": "1.7.2",
|
||||||
"1.0.1": "0.9.12",
|
"1.0.1": "0.9.12",
|
||||||
"1.0.0": "0.9.7"
|
"1.0.0": "0.9.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { defineConfig, mergeConfig } from "vitest/config";
|
|||||||
import { playwright } from "@vitest/browser-playwright";
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import viteConfig from "./vitest.config.common";
|
import viteConfig from "./vitest.config.common";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import dotenv from "dotenv";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { parseEnv } from "node:util";
|
||||||
import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands";
|
import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./test/lib/commands";
|
||||||
|
|
||||||
// P2P test environment variables
|
// P2P test environment variables
|
||||||
@@ -22,8 +23,9 @@ import { grantClipboardPermissions, writeHandoffFile, readHandoffFile } from "./
|
|||||||
// General test options (also read from env):
|
// General test options (also read from env):
|
||||||
// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests
|
// ENABLE_DEBUGGER - Set to "true" to attach a debugger and pause before tests
|
||||||
// ENABLE_UI - Set to "true" to open a visible browser window during tests
|
// ENABLE_UI - Set to "true" to open a visible browser window during tests
|
||||||
const defEnv = dotenv.config({ path: ".env" }).parsed;
|
const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined);
|
||||||
const testEnv = dotenv.config({ path: ".test.env" }).parsed;
|
const defEnv = loadEnvFile(".env");
|
||||||
|
const testEnv = loadEnvFile(".test.env");
|
||||||
// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence)
|
// Merge: dotenv files < process.env (so shell-injected vars like P2P_TEST_* take precedence)
|
||||||
const p2pEnv: Record<string, string> = {};
|
const p2pEnv: Record<string, string> = {};
|
||||||
if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID;
|
if (process.env.P2P_TEST_ROOM_ID) p2pEnv.P2P_TEST_ROOM_ID = process.env.P2P_TEST_ROOM_ID;
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { defineConfig, mergeConfig } from "vitest/config";
|
|||||||
import { playwright } from "@vitest/browser-playwright";
|
import { playwright } from "@vitest/browser-playwright";
|
||||||
import viteConfig from "./vitest.config.common";
|
import viteConfig from "./vitest.config.common";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import dotenv from "dotenv";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { parseEnv } from "node:util";
|
||||||
import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands";
|
import { grantClipboardPermissions, openWebPeer, closeWebPeer, acceptWebPeer } from "./test/lib/commands";
|
||||||
const defEnv = dotenv.config({ path: ".env" }).parsed;
|
|
||||||
const testEnv = dotenv.config({ path: ".test.env" }).parsed;
|
const loadEnvFile = (path: string) => (existsSync(path) ? parseEnv(readFileSync(path, "utf-8")) : undefined);
|
||||||
|
const defEnv = loadEnvFile(".env");
|
||||||
|
const testEnv = loadEnvFile(".test.env");
|
||||||
const env = Object.assign({}, defEnv, testEnv);
|
const env = Object.assign({}, defEnv, testEnv);
|
||||||
const debuggerEnabled = env?.ENABLE_DEBUGGER === "true";
|
const debuggerEnabled = env?.ENABLE_DEBUGGER === "true";
|
||||||
const enableUI = env?.ENABLE_UI === "true";
|
const enableUI = env?.ENABLE_UI === "true";
|
||||||
|
|||||||
Reference in New Issue
Block a user