From 104eeadb4bdcae070437a577c8323ca0b9e91f00 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Mon, 20 Jul 2026 18:08:09 +0000 Subject: [PATCH] Reduce Community lint type-safety warnings --- src/apps/cli/commands/p2p.ts | 2 +- src/apps/cli/lib/pouchdb-node.ts | 101 +++-- src/apps/cli/pouchdb-internals.d.ts | 21 + .../cli/services/NodeKeyValueDBService.ts | 41 +- src/apps/cli/vite.config.ts | 17 +- src/apps/webapp/main.ts | 6 +- src/apps/webapp/obsidianMock.ts | 371 +++++++++++------- src/apps/webapp/vaultSelector.ts | 27 +- src/apps/webapp/vite.config.ts | 17 +- src/common/KeyValueDB.ts | 3 +- src/common/couchdbConfiguration.ts | 40 ++ src/common/obsidianCommunityPlugins.ts | 62 +++ src/common/obsidianSettings.ts | 27 ++ src/features/ConfigSync/CmdConfigSync.ts | 55 ++- .../HiddenFileSync/CmdHiddenFileSync.ts | 32 +- .../configureHiddenFileSyncMode.ts | 13 +- src/features/optionalSyncFeatures.ts | 27 ++ src/modules/core/ReplicateResultProcessor.ts | 1 + .../coreObsidian/storageLib/utilObsidian.ts | 16 +- .../APILib/ObsHttpHandler.ts | 16 +- .../DocumentHistory/DocumentHistoryModal.ts | 1 + .../ConflictResolveModal.ts | 2 +- .../ModuleObsidianSettingAsMarkdown.ts | 16 +- .../features/ModuleObsidianSettingTab.ts | 7 +- .../ObsidianLiveSyncSettingTab.ts | 39 +- .../features/SettingDialogue/PaneChangeLog.ts | 2 +- .../SettingDialogue/utilFixCouchDBSetting.ts | 10 +- .../SetupWizard/dialogs/utilCheckCouchDB.ts | 3 +- .../services/ObsidianSettingService.ts | 7 +- 29 files changed, 664 insertions(+), 318 deletions(-) create mode 100644 src/apps/cli/pouchdb-internals.d.ts create mode 100644 src/common/couchdbConfiguration.ts create mode 100644 src/common/obsidianCommunityPlugins.ts create mode 100644 src/common/obsidianSettings.ts create mode 100644 src/features/optionalSyncFeatures.ts diff --git a/src/apps/cli/commands/p2p.ts b/src/apps/cli/commands/p2p.ts index 932ad3e1..c504ae01 100644 --- a/src/apps/cli/commands/p2p.ts +++ b/src/apps/cli/commands/p2p.ts @@ -189,7 +189,7 @@ export async function syncWithPeer( } const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId); if (!pushResult || pushResult.ok !== true) { - const err = pushResult?.error; + const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined; throw err instanceof Error ? err : LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync"); diff --git a/src/apps/cli/lib/pouchdb-node.ts b/src/apps/cli/lib/pouchdb-node.ts index af69d186..e1f1d210 100644 --- a/src/apps/cli/lib/pouchdb-node.ts +++ b/src/apps/cli/lib/pouchdb-node.ts @@ -8,11 +8,8 @@ import LevelDBAdapter from "pouchdb-adapter-leveldb"; import find from "pouchdb-find"; import transform from "transform-pouch"; -//@ts-ignore -import { findPathToLeaf } from "pouchdb-merge"; -//@ts-ignore +import { findPathToLeaf, type RevisionTreeNode } from "pouchdb-merge"; import { adapterFun } from "pouchdb-utils"; -//@ts-ignore import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors"; import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task"; @@ -28,8 +25,32 @@ type PurgeLogDocument = { purgeSeq: number; purges: Array<{ docId: string; rev: string; purgeSeq: number }>; }; +type PurgeMultiResultMap = Record; -function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) { +interface PouchDBPrivateDatabase extends PouchDB.Database { + adapter: string; + purged_infos_limit: number; + _getRevisionTree( + documentId: string, + callback: (error: Error | undefined, revisions?: RevisionTreeNode[]) => void + ): void; + _purge( + documentId: string, + revisionPath: string[], + callback: (error: Error | undefined, result?: PurgeMultiResult) => void + ): void; + purgeMulti(documents: PurgeMultiParam[]): Promise; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isSuccessfulPurge(value: unknown): value is PurgeMultiResult { + return isRecord(value) && value.ok === true; +} + +function appendPurgeSeqs(db: PouchDBPrivateDatabase, docs: PurgeMultiParam[]) { return db .get("_local/purges") .then(function (doc) { @@ -40,18 +61,16 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) { rev: rev$$1, purgeSeq, }); - //@ts-ignore : missing type def if (doc.purges.length > db.purged_infos_limit) { - //@ts-ignore : missing type def doc.purges.splice(0, doc.purges.length - db.purged_infos_limit); } doc.purgeSeq = purgeSeq; } return doc; }) - .catch(function (err) { - if (err.status !== 404) { - throw err; + .catch(function (error: unknown) { + if (!isRecord(error) || error.status !== 404) { + throw error; } return { _id: "_local/purges", @@ -71,68 +90,76 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) { /** * purge multiple documents at once. */ -PouchDB.prototype.purgeMulti = adapterFun( +const pouchDBPrototype = (PouchDB as typeof PouchDB & { prototype: PouchDBPrivateDatabase }).prototype; + +pouchDBPrototype.purgeMulti = adapterFun( "_purgeMulti", function ( + this: PouchDBPrivateDatabase, docs: PurgeMultiParam[], - callback: ( - error: Error, - result?: { - [x: string]: PurgeMultiResult | Error; - } - ) => void + callback: (error?: Error, result?: PurgeMultiResultMap) => void ) { - //@ts-ignore if (typeof this._purge === "undefined") { return callback( - //@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext. createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.") ); } - //@ts-ignore // eslint-disable-next-line @typescript-eslint/no-this-alias -- The adapter task callbacks must retain this PouchDB instance. const self = this; const tasks = docs.map( (param) => () => - new Promise<[PurgeMultiParam, PurgeMultiResult | Error]>((res, rej) => { + new Promise<[PurgeMultiParam, unknown]>((res) => { const [docId, rev$$1] = param; - self._getRevisionTree(docId, (error: Error, revs: string[]) => { + self._getRevisionTree(docId, (error, revs) => { if (error) { return res([param, error]); } if (!revs) { return res([param, createError(MISSING_DOC)]); } - let path; + let path: string[]; try { path = findPathToLeaf(revs, rev$$1); - } catch (error) { - //@ts-ignore - return res([param, error.message || error]); + } catch (caught: unknown) { + const failure = caught instanceof Error && caught.message ? caught.message : caught; + return res([param, failure]); } - self._purge(docId, path, (error: Error, result: PurgeMultiResult) => { + self._purge(docId, path, (error, result) => { if (error) { return res([param, error]); - } else { - return res([param, result]); } + return res([param, result]); }); }); }) ); (async () => { const ret = await mapAllTasksWithConcurrencyLimit(1, tasks); - const retAll = ret.map((e) => unwrapTaskResult(e)) as [PurgeMultiParam, PurgeMultiResult | Error][]; - await appendPurgeSeqs( - self, - retAll.filter((e) => "ok" in e[1]).map((e) => e[0]) - ); - const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]])); + const retAll: Array<[PurgeMultiParam, unknown]> = []; + for (const entry of ret) { + const outcome = unwrapTaskResult(entry); + if (outcome instanceof Error) { + throw outcome; + } + retAll.push(outcome); + } + const successfullyPurged: PurgeMultiParam[] = []; + const resultEntries: Array<[string, unknown]> = []; + for (const [document, outcome] of retAll) { + if (isSuccessfulPurge(outcome)) { + successfullyPurged.push(document); + } + resultEntries.push([document[0], outcome]); + } + await appendPurgeSeqs(self, successfullyPurged); + const result: PurgeMultiResultMap = Object.fromEntries(resultEntries); return result; })() - //@ts-ignore .then((result) => callback(undefined, result)) - .catch((error) => callback(error)); + .catch((caught: unknown) => { + const error = caught instanceof Error ? caught : new Error(String(caught)); + callback(error); + }); } ); diff --git a/src/apps/cli/pouchdb-internals.d.ts b/src/apps/cli/pouchdb-internals.d.ts new file mode 100644 index 00000000..4d0885ec --- /dev/null +++ b/src/apps/cli/pouchdb-internals.d.ts @@ -0,0 +1,21 @@ +declare module "pouchdb-merge" { + export interface RevisionTreeNode { + pos: number; + ids: [revision: string, metadata: Record, branches: RevisionTreeNode["ids"][]]; + } + + export function findPathToLeaf(revisions: RevisionTreeNode[], targetRevision: string): string[]; +} + +declare module "pouchdb-utils" { + export function adapterFun( + name: string, + callback: (this: TThis, ...args: [...TArguments, callback: (error?: Error, result?: TResult) => void]) => void + ): (this: TThis, ...args: TArguments) => Promise; +} + +declare module "pouchdb-errors" { + export const MISSING_DOC: unknown; + export const UNKNOWN_ERROR: unknown; + export function createError(error: unknown, reason?: string): Error; +} diff --git a/src/apps/cli/services/NodeKeyValueDBService.ts b/src/apps/cli/services/NodeKeyValueDBService.ts index 33386c55..48bbb78d 100644 --- a/src/apps/cli/services/NodeKeyValueDBService.ts +++ b/src/apps/cli/services/NodeKeyValueDBService.ts @@ -82,6 +82,17 @@ function deserializeFromNodeKV(value: unknown): unknown { return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFromNodeKV(v)])); } +function asKeyString(key: unknown): string { + if (typeof key === "string") { + return key; + } + const serialised = JSON.stringify(key); + if (typeof serialised !== "string") { + throw new TypeError("The IndexedDB key could not be serialised"); + } + return serialised; +} + class NodeFileKeyValueDatabase implements KeyValueDatabase { private filePath: string; private data = new Map(); @@ -91,13 +102,6 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase { this.load(); } - private asKeyString(key: IDBValidKey): string { - if (typeof key === "string") { - return key; - } - return JSON.stringify(key); - } - private load() { try { const loaded = JSON.parse(nodeFs.readFileSync(this.filePath, "utf-8")) as Record; @@ -116,17 +120,17 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase { } async get(key: IDBValidKey): Promise { - return this.data.get(this.asKeyString(key)) as T; + return this.data.get(asKeyString(key)) as T; } async set(key: IDBValidKey, value: T): Promise { - this.data.set(this.asKeyString(key), value); + this.data.set(asKeyString(key), value); this.flush(); return key; } async del(key: IDBValidKey): Promise { - this.data.delete(this.asKeyString(key)); + this.data.delete(asKeyString(key)); this.flush(); } @@ -144,11 +148,12 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase { let filtered = allKeys; if (typeof query !== "undefined") { if (this.isIDBKeyRangeLike(query)) { - const lower = query.lower?.toString() ?? ""; - const upper = query.upper?.toString() ?? "\uffff"; + const lower = query.lower === undefined ? "" : String(query.lower); + const upper = query.upper === undefined ? "\uffff" : String(query.upper); filtered = filtered.filter((key) => key >= lower && key <= upper); } else { - const exact = query.toString(); + const exactValue: unknown = query; + const exact = String(exactValue); filtered = filtered.filter((key) => key === exact); } } @@ -272,7 +277,15 @@ export class NodeKeyValueDBService await getDB().del(`${prefix}${key}`); }, keys: async (from: string | undefined, to: string | undefined, count?: number): Promise => { - const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString()); + const rawKeys: unknown = await getDB().keys(undefined, count); + if (!Array.isArray(rawKeys)) { + throw new TypeError("The key-value database returned an invalid key list"); + } + const keyList: unknown[] = rawKeys; + const allKeys: string[] = []; + for (const key of keyList) { + allKeys.push(String(key)); + } const lower = `${prefix}${from ?? ""}`; const upper = `${prefix}${to ?? "\uffff"}`; return allKeys diff --git a/src/apps/cli/vite.config.ts b/src/apps/cli/vite.config.ts index f0431741..a3212a34 100644 --- a/src/apps/cli/vite.config.ts +++ b/src/apps/cli/vite.config.ts @@ -4,8 +4,17 @@ import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/n const __dirname = path.dirname(fileURLToPath(import.meta.url)); const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/"); const repoRoot = path.resolve(__dirname, "../../.."); -const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8")); -const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8")); + +function readVersion(filePath: string): string | undefined { + const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8")); + if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) { + return undefined; + } + return typeof parsed.version === "string" ? parsed.version : undefined; +} + +const packageVersion = readVersion(path.resolve(repoRoot, "package.json")); +const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json")); // https://vite.dev/config/ const defaultExternal = [ "obsidian", @@ -113,7 +122,7 @@ export default defineConfig({ global: "globalThis", nonInteractive: "true", // localStorage: "undefined", // Prevent usage of localStorage in the CLI environment - MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"), - PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"), + MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"), + PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"), }, }); diff --git a/src/apps/webapp/main.ts b/src/apps/webapp/main.ts index 8575c8b1..a263df81 100644 --- a/src/apps/webapp/main.ts +++ b/src/apps/webapp/main.ts @@ -176,7 +176,11 @@ class LiveSyncWebApp { const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE); const file = await fileHandle.getFile(); const text = await file.text(); - return JSON.parse(text); + const parsed: unknown = JSON.parse(text); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + throw new Error("The WebApp settings file does not contain an object"); + } + return parsed; } catch { // File doesn't exist yet return null; diff --git a/src/apps/webapp/obsidianMock.ts b/src/apps/webapp/obsidianMock.ts index e494d2a1..16e563f9 100644 --- a/src/apps/webapp/obsidianMock.ts +++ b/src/apps/webapp/obsidianMock.ts @@ -5,16 +5,43 @@ * Obsidian mock, and must not become a general test environment for the plug-in. When the Webapp compatibility boundary * is redesigned, replace this implementation here rather than extending it as a shared Obsidian simulation. */ -export const SettingCache = new Map(); -//@ts-ignore obsidian global -globalThis.activeDocument = document; +import type { + Command, + DataWriteOptions, + ListedFiles, + MarkdownFileInfo, + PluginManifest, + RequestUrlParam, + RequestUrlResponse, + ValueComponent, +} from "obsidian"; + +export type { + DataWriteOptions, + ListedFiles, + MarkdownFileInfo, + PluginManifest, + RequestUrlParam, + RequestUrlResponse, + ValueComponent, +}; + +type EventCallback = (...args: unknown[]) => unknown; + +declare global { + interface Window { + activeDocument: Document; + } +} + +export const SettingCache = new Map(); +window.activeDocument = document; declare const hostPlatform: string | undefined; -globalThis.process = { - platform: (hostPlatform || "win32") as any, -} as any; -console.warn(`[Obsidian Mock] process.platform is set to ${globalThis.process.platform}`); +Reflect.set(window, "process", { + platform: hostPlatform || "win32", +}); export class TAbstractFile { vault: Vault; path: string; @@ -55,7 +82,12 @@ export class TFolder extends TAbstractFile { } } -export class EventRef {} +export class EventRef { + constructor( + readonly name: string, + readonly callback: EventCallback + ) {} +} // class StorageMap extends Map { // constructor(saveName?: string) { @@ -114,7 +146,7 @@ export class Vault { private files: Map = new Map(); private contents: Map = new Map(); private root: TFolder; - private listeners: Map any>> = new Map(); + private listeners: Map> = new Map(); constructor(vaultName?: string) { if (vaultName) { @@ -194,19 +226,16 @@ export class Vault { if (this.files.has(path)) throw new Error("File already exists"); const name = path.split("/").pop() || ""; const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ""; - let parent = this.getAbstractFileByPath(parentPath); - if (!parent || !(parent instanceof TFolder)) { - parent = await this.createFolder(parentPath); - } + const existingParent = this.getAbstractFileByPath(parentPath); + const parent = existingParent instanceof TFolder ? existingParent : await this.createFolder(parentPath); - const file = new TFile(this, path, name, parent as TFolder); + const file = new TFile(this, path, name, parent); file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength; file.stat.ctime = options?.ctime ?? Date.now(); file.stat.mtime = options?.mtime ?? Date.now(); this.files.set(path, file); this.contents.set(path, data); - (parent as TFolder).children.push(file); - // console.dir(this.files); + parent.children.push(file); this.trigger("create", file); return file; @@ -224,7 +253,6 @@ export class Vault { file.stat.mtime = options?.mtime ?? Date.now(); file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now(); file.stat.size = typeof data === "string" ? data.length : data.byteLength; - console.warn(`[Obsidian Mock ${this.vaultName}] Modified file at path: '${file.path}'`); this.files.set(file.path, file); this.trigger("modify", file); } @@ -252,6 +280,14 @@ export class Vault { } async delete(file: TAbstractFile, force?: boolean): Promise { + return this.removeFile(file); + } + + async trash(file: TAbstractFile, system: boolean): Promise { + return this.removeFile(file); + } + + private async removeFile(file: TAbstractFile): Promise { await Promise.resolve(); this.files.delete(file.path); this.contents.delete(file.path); @@ -261,30 +297,27 @@ export class Vault { this.trigger("delete", file); } - async trash(file: TAbstractFile, system: boolean): Promise { - await Promise.resolve(); - return this.delete(file); + async removeFromAdapter(file: TAbstractFile): Promise { + return this.removeFile(file); } - on(name: string, callback: (...args: any[]) => any, ctx?: any): EventRef { - if (!this.listeners.has(name)) { - this.listeners.set(name, new Set()); - } - const boundCallback = ctx ? callback.bind(ctx) : callback; - this.listeners.get(name)!.add(boundCallback); - return { name, callback: boundCallback } as any; + on(name: string, callback: EventCallback, ctx?: object): EventRef { + const listeners = this.listeners.get(name) ?? new Set(); + this.listeners.set(name, listeners); + const boundCallback = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback; + listeners.add(boundCallback); + return new EventRef(name, boundCallback); } - off(name: string, callback: any) { + off(name: string, callback: EventCallback) { this.listeners.get(name)?.delete(callback); } offref(ref: EventRef) { - const { name, callback } = ref as any; - this.off(name, callback); + this.off(ref.name, ref.callback); } - trigger(name: string, ...args: any[]) { + trigger(name: string, ...args: unknown[]) { this.listeners.get(name)?.forEach((cb) => cb(...args)); } @@ -366,28 +399,29 @@ export class DataAdapter { } async remove(path: string): Promise { const file = this.vault.getAbstractFileByPath(path); - if (file) await this.vault.delete(file); + if (file) await this.vault.removeFromAdapter(file); } } class Events { _eventEmitter = new EventTarget(); - _events = new Map(); - _eventTarget(cb: any) { - const x = this._events.get(cb); - if (x) { - return x; + _events = new Map(); + _eventTarget(callback: EventCallback): EventListener { + const registered = this._events.get(callback); + if (registered) { + return registered; } - const callback = (evt: any) => { - x(evt?.detail ?? undefined); + const eventListener = (event: Event) => { + callback(event instanceof CustomEvent ? event.detail : undefined); }; - this._events.set(cb, callback); - return callback; + this._events.set(callback, eventListener); + return eventListener; } - on(name: string, cb: any, ctx?: any) { - this._eventEmitter.addEventListener(name, this._eventTarget(cb)); + on(name: string, callback: EventCallback, ctx?: object) { + const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback; + this._eventEmitter.addEventListener(name, this._eventTarget(registered)); } - trigger(name: string, args: any) { + trigger(name: string, args: unknown) { const evt = new CustomEvent(name, { detail: args, }); @@ -403,13 +437,13 @@ class Workspace extends Events { return null; } - onLayoutReady(cb: any) { + onLayoutReady(callback: () => void) { // cb(); // console.log("[Obsidian Mock] Workspace onLayoutReady registered"); // this._eventEmitter.addEventListener("layout-ready", () => { // console.log("[Obsidian Mock] Workspace layout-ready event triggered"); - setTimeout(() => { - cb(); + window.setTimeout(() => { + callback(); }, 200); // }); } @@ -434,33 +468,36 @@ export class App { } vault: Vault; workspace: Workspace = new Workspace(); - metadataCache: any = { - on: (name: string, cb: any, ctx?: any) => {}, + metadataCache = { + on: (name: string, callback: EventCallback, ctx?: object): EventRef => { + const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback; + return new EventRef(name, registered); + }, getFileCache: (): null => null, }; } export class Plugin { app: App; - manifest: any; - settings: any; - commands: Map = new Map(); - constructor(app: App, manifest: any) { + manifest: PluginManifest; + settings: unknown; + commands: Map = new Map(); + constructor(app: App, manifest: PluginManifest) { this.app = app; this.manifest = manifest; } - async loadData(): Promise { + async loadData(): Promise { await Promise.resolve(); return SettingCache.get(this.app) ?? {}; } - async saveData(data: any): Promise { + async saveData(data: unknown): Promise { await Promise.resolve(); SettingCache.set(this.app, data); } onload() {} onunload() {} - addSettingTab(tab: any) {} - addCommand(command: any) { + addSettingTab(tab: PluginSettingTab) {} + addCommand(command: Command) { this.commands.set(command.id, command); } addStatusBarItem() { @@ -478,10 +515,14 @@ export class Plugin { }; return icon; } - registerView(type: string, creator: any) {} - registerObsidianProtocolHandler(handler: any) {} - registerEvent(handler: any) {} - registerDomEvent(target: any, eventName: string, handler: any) {} + registerView(type: string, creator: () => ItemView) {} + registerObsidianProtocolHandler(handler: (params: Record) => unknown) {} + registerEvent(handler: EventRef) {} + registerDomEvent( + target: HTMLElement, + eventName: K, + handler: (event: HTMLElementEventMap[K]) => unknown + ) {} } export class Notice { @@ -489,11 +530,8 @@ export class Notice { private static _counter = 0; constructor(message: string) { this._key = Notice._counter++; - console.log(`Notice [${this._key}]:`, message); - } - setMessage(message: string) { - console.log(`Notice [${this._key}]:`, message); } + setMessage(message: string) {} } export class Modal { @@ -558,7 +596,7 @@ export const Platform = { }; export class Menu { - addItem(cb: (item: MenuItem) => any) { + addItem(cb: (item: MenuItem) => unknown) { cb(new MenuItem()); return this; } @@ -571,7 +609,7 @@ export class MenuItem { setIcon(icon: string) { return this; } - onClick(cb: (evt: MouseEvent) => any) { + onClick(cb: (evt: MouseEvent) => unknown) { return this; } } @@ -584,7 +622,7 @@ export class Component { export class ButtonComponent extends Component { buttonEl: HTMLButtonElement = document.createElement("button"); - private clickHandler: ((evt: MouseEvent) => any) | null = null; + private clickHandler: ((evt: MouseEvent) => unknown) | null = null; constructor() { super(); @@ -602,7 +640,7 @@ export class ButtonComponent extends Component { return this; } - onClick(cb: (evt: MouseEvent) => any) { + onClick(cb: (evt: MouseEvent) => unknown) { this.clickHandler = cb; this.buttonEl.removeEventListener("click", this.clickHandler); this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent)); @@ -627,7 +665,7 @@ export class ButtonComponent extends Component { export class TextComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); - private changeHandler: ((value: string) => any) | null = null; + private changeHandler: ((value: string) => unknown) | null = null; constructor() { super(); @@ -635,7 +673,7 @@ export class TextComponent extends Component { this.inputEl.type = "text"; } - onChange(cb: (value: string) => any) { + onChange(cb: (value: string) => unknown) { this.changeHandler = cb; this.inputEl.removeEventListener("change", this.handleChange); this.inputEl.addEventListener("change", this.handleChange); @@ -671,7 +709,7 @@ export class TextComponent extends Component { export class ToggleComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); - private changeHandler: ((value: boolean) => any) | null = null; + private changeHandler: ((value: boolean) => unknown) | null = null; constructor() { super(); @@ -679,7 +717,7 @@ export class ToggleComponent extends Component { this.inputEl.type = "checkbox"; } - onChange(cb: (value: boolean) => any) { + onChange(cb: (value: boolean) => unknown) { this.changeHandler = cb; this.inputEl.addEventListener("change", (evt) => { const target = evt.target as HTMLInputElement; @@ -701,7 +739,7 @@ export class ToggleComponent extends Component { export class DropdownComponent extends Component { selectEl: HTMLSelectElement = document.createElement("select"); - private changeHandler: ((value: string) => any) | null = null; + private changeHandler: ((value: string) => unknown) | null = null; constructor() { super(); @@ -723,7 +761,7 @@ export class DropdownComponent extends Component { return this; } - onChange(cb: (value: string) => any) { + onChange(cb: (value: string) => unknown) { this.changeHandler = cb; this.selectEl.addEventListener("change", (evt) => { const target = evt.target as HTMLSelectElement; @@ -745,7 +783,7 @@ export class DropdownComponent extends Component { export class SliderComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); - private changeHandler: ((value: number) => any) | null = null; + private changeHandler: ((value: number) => unknown) | null = null; constructor() { super(); @@ -753,7 +791,7 @@ export class SliderComponent extends Component { this.inputEl.type = "range"; } - onChange(cb: (value: number) => any) { + onChange(cb: (value: number) => unknown) { this.changeHandler = cb; this.inputEl.addEventListener("change", (evt) => { const target = evt.target as HTMLInputElement; @@ -816,72 +854,115 @@ export class Setting { this.controlEl.addClass(c); return this; } - addText(cb: (text: TextComponent) => any) { + addText(cb: (text: TextComponent) => unknown) { const component = new TextComponent(); this.controlEl.appendChild(component.inputEl); cb(component); return this; } - addToggle(cb: (toggle: ToggleComponent) => any) { + addToggle(cb: (toggle: ToggleComponent) => unknown) { const component = new ToggleComponent(); cb(component); return this; } - addButton(cb: (btn: ButtonComponent) => any) { + addButton(cb: (btn: ButtonComponent) => unknown) { const btn = new ButtonComponent(); this.controlEl.appendChild(btn.buttonEl); cb(btn); return this; } - addDropdown(cb: (dropdown: DropdownComponent) => any) { + addDropdown(cb: (dropdown: DropdownComponent) => unknown) { const component = new DropdownComponent(); cb(component); return this; } - addSlider(cb: (slider: SliderComponent) => any) { + addSlider(cb: (slider: SliderComponent) => unknown) { const component = new SliderComponent(); cb(component); return this; } } -// HTMLElement extensions +function applyDomElementInfo(element: HTMLElement, info?: DomElementInfo | string): void { + if (typeof info === "string") { + element.textContent = info; + return; + } + if (!info) return; + if (info.cls) { + const classes = Array.isArray(info.cls) ? info.cls : info.cls.split(" "); + element.classList.add(...classes.filter((className) => className !== "")); + } + if (info.text !== undefined) { + element.replaceChildren(info.text); + } + if (info.attr) { + for (const [name, value] of Object.entries(info.attr)) { + if (value === null) element.removeAttribute(name); + else element.setAttribute(name, String(value)); + } + } + if (info.title !== undefined) element.title = info.title; + if (info.value !== undefined && "value" in element) element.value = info.value; + if (info.type !== undefined && "type" in element) element.type = info.type; + if (info.placeholder !== undefined && "placeholder" in element) element.placeholder = info.placeholder; + if (info.href !== undefined) element.setAttribute("href", info.href); +} + +// HTMLElement extensions used by the Webapp compatibility implementation. if (typeof HTMLElement !== "undefined") { - const proto = HTMLElement.prototype as any; - proto.createDiv = function (o?: any) { - const div = document.createElement("div"); - if (o?.cls) div.addClass(o.cls); - if (o?.text) div.setText(o.text); - this.appendChild(div); - return div; + const proto = HTMLElement.prototype; + proto.createDiv = function ( + this: HTMLElement, + info?: DomElementInfo | string, + callback?: (element: HTMLDivElement) => void + ): HTMLDivElement { + const element = document.createElement("div"); + applyDomElementInfo(element, info); + this.appendChild(element); + callback?.(element); + return element; }; - proto.createEl = function (tag: string, o?: any) { - const el = document.createElement(tag); - if (o?.cls) el.addClass(o.cls); - if (o?.text) el.setText(o.text); - this.appendChild(el); - return el; + proto.createEl = function ( + this: HTMLElement, + tag: K, + info?: DomElementInfo | string, + callback?: (element: HTMLElementTagNameMap[K]) => void + ): HTMLElementTagNameMap[K] { + const element = document.createElement(tag); + applyDomElementInfo(element, info); + this.appendChild(element); + callback?.(element); + return element; }; - proto.createSpan = function (o?: any) { - return this.createEl("span", o); + proto.createSpan = function ( + this: HTMLElement, + info?: DomElementInfo | string, + callback?: (element: HTMLSpanElement) => void + ): HTMLSpanElement { + const element = document.createElement("span"); + applyDomElementInfo(element, info); + this.appendChild(element); + callback?.(element); + return element; }; - proto.empty = function () { - this.innerHTML = ""; + proto.empty = function (this: HTMLElement): void { + this.replaceChildren(); }; - proto.setText = function (t: string) { - this.textContent = t; + proto.setText = function (this: HTMLElement, text: string): void { + this.textContent = text; }; - proto.addClass = function (c: string) { - this.classList.add(c); + proto.addClass = function (this: HTMLElement, className: string): void { + this.classList.add(className); }; - proto.removeClass = function (c: string) { - this.classList.remove(c); + proto.removeClass = function (this: HTMLElement, className: string): void { + this.classList.remove(className); }; - proto.toggleClass = function (c: string, b: boolean) { - this.classList.toggle(c, b); + proto.toggleClass = function (this: HTMLElement, className: string, value: boolean): void { + this.classList.toggle(className, value); }; - proto.hasClass = function (c: string) { - return this.classList.contains(c); + proto.hasClass = function (this: HTMLElement, className: string): boolean { + return this.classList.contains(className); }; } @@ -896,10 +977,19 @@ export class FuzzySuggestModal { throw new Error("Not implemented."); } } + +function parseHtmlFragment(html: string): DocumentFragment { + const parsed = new DOMParser().parseFromString(html, "text/html"); + const fragment = document.createDocumentFragment(); + for (const child of [...parsed.body.childNodes]) { + fragment.appendChild(document.importNode(child, true)); + } + return fragment; +} + export class MarkdownRenderer { static render(app: App, md: string, el: HTMLElement, path: string, component: Component) { - // eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's trusted Markdown renderer boundary. - el.innerHTML = md; + el.replaceChildren(parseHtmlFragment(md)); return Promise.resolve(); } } @@ -910,26 +1000,23 @@ export class WorkspaceLeaf {} export function sanitizeHTMLToDom(html: string) { const div = document.createElement("div"); - // eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's sanitised-HTML API contract. - div.innerHTML = html; + div.appendChild(parseHtmlFragment(html)); return div; } export function addIcon() {} -export const debounce = (fn: any) => fn; -export async function request(options: any) { +export function debounce( + fn: (...args: Arguments) => Result +): (...args: Arguments) => Result { + return fn; +} +export async function request(options: RequestUrlParam | string): Promise { const result = await requestUrl(options); return result.text; } -export async function requestUrl({ - body, - headers, - method, - url, - contentType, -}: RequestUrlParam): Promise { - // console.log("[requestUrl] Mock called:", { method, url, contentType }); +export async function requestUrl(options: RequestUrlParam | string): Promise { + const { body, headers, method, url, contentType } = typeof options === "string" ? { url: options } : options; const reqHeadersObj: Record = {}; for (const key of Object.keys(headers || {})) { reqHeadersObj[key.toLowerCase()] = headers[key]; @@ -940,21 +1027,21 @@ export async function requestUrl({ reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate"; reqHeadersObj["Pragma"] = "no-cache"; reqHeadersObj["Expires"] = "0"; - const result = await fetch(url, { - method: method, + const result = await window.fetch(url, { + method, headers: { ...reqHeadersObj, }, - body: body, + body, }); const headersObj: Record = {}; result.headers.forEach((value, key) => { headersObj[key] = value; }); - let json = undefined; - let text = undefined; - let arrayBuffer = undefined; + let json: unknown; + let text = ""; + let arrayBuffer = new ArrayBuffer(0); try { const isJson = result.headers.get("content-type")?.includes("application/json"); arrayBuffer = await result.arrayBuffer(); @@ -965,9 +1052,8 @@ export async function requestUrl({ if (isJson) { json = await JSON.parse(text || "{}"); } - } catch (e) { - console.warn("Failed to parse response:", e); - // ignore + } catch { + json = undefined; } return { status: result.status, @@ -977,11 +1063,12 @@ export async function requestUrl({ arrayBuffer: arrayBuffer, }; } -export function stringifyYaml(obj: any) { +export function stringifyYaml(obj: unknown): string { return JSON.stringify(obj); } -export function parseYaml(s: string) { - return JSON.parse(s); +export function parseYaml(s: string): unknown { + const parsed: unknown = JSON.parse(s); + return parsed; } export function getLanguage() { return "en"; @@ -997,15 +1084,3 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string { export function base64ToArrayBuffer(base64: string): ArrayBuffer { return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer; } - -export type DataWriteOptions = any; -export type PluginManifest = any; -export type RequestUrlParam = any; -export type RequestUrlResponse = any; -export type MarkdownFileInfo = any; -export type ListedFiles = { - files: string[]; - folders: string[]; -}; - -export type ValueComponent = any; diff --git a/src/apps/webapp/vaultSelector.ts b/src/apps/webapp/vaultSelector.ts index d5d3e066..7c2d2321 100644 --- a/src/apps/webapp/vaultSelector.ts +++ b/src/apps/webapp/vaultSelector.ts @@ -15,6 +15,18 @@ export type VaultHistoryItem = { type VaultHistoryValue = VaultHistoryItem; +function isVaultHistoryValue(value: unknown): value is VaultHistoryValue { + if (typeof value !== "object" || value === null) return false; + const candidate = value as Partial; + return ( + typeof candidate.id === "string" && + typeof candidate.name === "string" && + typeof candidate.handle === "object" && + candidate.handle !== null && + typeof candidate.lastUsedAt === "number" + ); +} + function makeVaultKey(id: string): string { return `${VAULT_KEY_PREFIX}${id}`; } @@ -87,7 +99,7 @@ export class VaultHistoryStore { async getLastUsedVaultId(): Promise { return this.withStore("readonly", async (store) => { - const value = await this.requestAsPromise(store.get(LAST_USED_KEY)); + const value: unknown = await this.requestAsPromise(store.get(LAST_USED_KEY)); return typeof value === "string" ? value : null; }); } @@ -95,20 +107,21 @@ export class VaultHistoryStore { async getVaultHistory(): Promise { return this.withStore("readonly", async (store) => { const keys = await this.requestAsPromise(store.getAllKeys()); - const values = (await this.requestAsPromise(store.getAll())) as unknown[]; + const values = await this.requestAsPromise(store.getAll() as IDBRequest); const items: VaultHistoryItem[] = []; for (let i = 0; i < keys.length; i++) { - const key = String(keys[i]); + const key = keys[i]; + if (typeof key !== "string") continue; const id = parseVaultId(key); - const value = values[i] as Partial | undefined; - if (!id || !value || !value.handle || !value.name) { + const value = values[i]; + if (!id || !isVaultHistoryValue(value)) { continue; } items.push({ id, - name: String(value.name), + name: value.name, handle: value.handle, - lastUsedAt: Number(value.lastUsedAt || 0), + lastUsedAt: value.lastUsedAt, }); } items.sort((a, b) => b.lastUsedAt - a.lastUsedAt); diff --git a/src/apps/webapp/vite.config.ts b/src/apps/webapp/vite.config.ts index 2eddc644..c0d8d2e5 100644 --- a/src/apps/webapp/vite.config.ts +++ b/src/apps/webapp/vite.config.ts @@ -4,8 +4,17 @@ import istanbul from "vite-plugin-istanbul"; import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, "../../.."); -const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8")); -const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8")); + +function readVersion(filePath: string): string | undefined { + const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8")); + if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) { + return undefined; + } + return typeof parsed.version === "string" ? parsed.version : undefined; +} + +const packageVersion = readVersion(path.resolve(repoRoot, "package.json")); +const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json")); const enableCoverage = process.env.PW_COVERAGE === "1"; // https://vite.dev/config/ export default defineConfig({ @@ -57,8 +66,8 @@ export default defineConfig({ }, }, define: { - MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"), - PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"), + MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"), + PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"), global: "globalThis", hostPlatform: JSON.stringify(process.platform || "linux"), }, diff --git a/src/common/KeyValueDB.ts b/src/common/KeyValueDB.ts index 3cd5b612..47a75c70 100644 --- a/src/common/KeyValueDB.ts +++ b/src/common/KeyValueDB.ts @@ -52,7 +52,8 @@ export const _OpenKeyValueDatabase = async (dbKey: string): Promise(key: IDBValidKey, value: T) { if (!db) { diff --git a/src/common/couchdbConfiguration.ts b/src/common/couchdbConfiguration.ts new file mode 100644 index 00000000..4b4874a6 --- /dev/null +++ b/src/common/couchdbConfiguration.ts @@ -0,0 +1,40 @@ +type CouchDBConfigurationSection = Record; + +export interface CouchDBConfiguration { + admins: CouchDBConfigurationSection; + chttpd: CouchDBConfigurationSection; + chttpd_auth: CouchDBConfigurationSection; + couchdb: CouchDBConfigurationSection; + cors: CouchDBConfigurationSection; + httpd: CouchDBConfigurationSection; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function normaliseSection(value: unknown): CouchDBConfigurationSection { + if (!isRecord(value)) { + return {}; + } + + const entries: Array<[string, string]> = []; + for (const [key, entry] of Object.entries(value)) { + if (typeof entry === "string") { + entries.push([key, entry]); + } + } + return Object.fromEntries(entries); +} + +export function normaliseCouchDBConfiguration(value: unknown): CouchDBConfiguration { + const source = isRecord(value) ? value : {}; + return { + admins: normaliseSection(source.admins), + chttpd: normaliseSection(source.chttpd), + chttpd_auth: normaliseSection(source.chttpd_auth), + couchdb: normaliseSection(source.couchdb), + cors: normaliseSection(source.cors), + httpd: normaliseSection(source.httpd), + }; +} diff --git a/src/common/obsidianCommunityPlugins.ts b/src/common/obsidianCommunityPlugins.ts new file mode 100644 index 00000000..17ae52a7 --- /dev/null +++ b/src/common/obsidianCommunityPlugins.ts @@ -0,0 +1,62 @@ +import type { App, PluginManifest } from "@/deps.ts"; + +export interface ObsidianCommunityPluginManager { + enabledPlugins: ReadonlySet; + manifests: PluginManifest[]; + loadPlugin(pluginId: string): Promise; + unloadPlugin(pluginId: string): Promise; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isPluginManifest(value: unknown): value is PluginManifest { + return ( + isRecord(value) && + typeof value.id === "string" && + typeof value.name === "string" && + (value.dir === undefined || typeof value.dir === "string") + ); +} + +function isStringSet(value: unknown): value is ReadonlySet { + if (!(value instanceof Set)) { + return false; + } + const entries = value as ReadonlySet; + for (const entry of entries) { + if (typeof entry !== "string") { + return false; + } + } + return true; +} + +async function invokeLifecycleMethod( + manager: Record, + methodName: "loadPlugin" | "unloadPlugin", + pluginId: string +): Promise { + const method = manager[methodName]; + if (typeof method !== "function") { + throw new TypeError(`Obsidian does not expose ${methodName}`); + } + const result: unknown = Reflect.apply(method, manager, [pluginId]); + await result; +} + +export function getObsidianCommunityPluginManager(app: App): ObsidianCommunityPluginManager { + const managerValue: unknown = Reflect.get(app, "plugins"); + if (!isRecord(managerValue) || !isRecord(managerValue.manifests) || !isStringSet(managerValue.enabledPlugins)) { + throw new TypeError("Obsidian does not expose the community plug-in manager"); + } + + const manifests = Object.values(managerValue.manifests).filter(isPluginManifest); + return { + enabledPlugins: managerValue.enabledPlugins, + manifests, + loadPlugin: async (pluginId) => await invokeLifecycleMethod(managerValue, "loadPlugin", pluginId), + unloadPlugin: async (pluginId) => await invokeLifecycleMethod(managerValue, "unloadPlugin", pluginId), + }; +} diff --git a/src/common/obsidianSettings.ts b/src/common/obsidianSettings.ts new file mode 100644 index 00000000..7bb09e91 --- /dev/null +++ b/src/common/obsidianSettings.ts @@ -0,0 +1,27 @@ +import type { App } from "@/deps.ts"; + +function getSettingsManager(app: App): Record { + const manager: unknown = Reflect.get(app, "setting"); + if (typeof manager !== "object" || manager === null) { + throw new TypeError("Obsidian does not expose the settings manager"); + } + return manager as Record; +} + +function invokeSettingsMethod(app: App, methodName: string, args: unknown[] = []): void { + const manager = getSettingsManager(app); + const method = manager[methodName]; + if (typeof method !== "function") { + throw new TypeError(`Obsidian does not expose settings.${methodName}`); + } + Reflect.apply(method, manager, args); +} + +export function openObsidianSettings(app: App, tabId: string): void { + invokeSettingsMethod(app, "open"); + invokeSettingsMethod(app, "openTabById", [tabId]); +} + +export function closeObsidianSettings(app: App): void { + invokeSettingsMethod(app, "close"); +} diff --git a/src/features/ConfigSync/CmdConfigSync.ts b/src/features/ConfigSync/CmdConfigSync.ts index 7f6c0098..035bf245 100644 --- a/src/features/ConfigSync/CmdConfigSync.ts +++ b/src/features/ConfigSync/CmdConfigSync.ts @@ -1,4 +1,5 @@ import { writable } from "svelte/store"; +import type PouchDB from "pouchdb-core"; import { Notice, type PluginManifest, @@ -76,18 +77,12 @@ import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n"; import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub"; import type { LiveSyncCore } from "@/main.ts"; import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError"; +import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts"; +import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts"; const d = "\u200b"; const d2 = "\n"; -declare global { - interface OPTIONAL_SYNC_FEATURES { - DISABLE: "DISABLE"; - CUSTOMIZE: "CUSTOMIZE"; - DISABLE_CUSTOM: "DISABLE_CUSTOM"; - } -} - function serialize(data: PluginDataEx): string { // For higher performance, create custom plug-in data strings. // Self-hosted LiveSync uses `\n` to split chunks. Therefore, grouping together those with similar entropy would work nicely. @@ -254,7 +249,8 @@ function deserialize(str: string[], def: T) { return JSON.parse(str.join("")) as T; } catch { try { - return parseYaml(str.join("")); + const parsed: unknown = parseYaml(str.join("")); + return parsed as T; } catch { return def; } @@ -1104,12 +1100,11 @@ export class ConfigSync extends LiveSyncCommands { await delay(100); this._log(`Config ${data.displayName || data.name} has been applied`, LOG_LEVEL_NOTICE); if (data.category == "PLUGIN_DATA" || data.category == "PLUGIN_MAIN") { - //@ts-ignore - const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[]; - //@ts-ignore - const enabledPlugins = this.app.plugins.enabledPlugins as Set; - const pluginManifest = manifests.find( - (manifest) => enabledPlugins.has(manifest.id) && manifest.dir == `${baseDir}/plugins/${data.name}` + const pluginManager = getObsidianCommunityPluginManager(this.app); + const pluginManifest = pluginManager.manifests.find( + (manifest) => + pluginManager.enabledPlugins.has(manifest.id) && + manifest.dir == `${baseDir}/plugins/${data.name}` ); if (pluginManifest) { this._log( @@ -1117,10 +1112,8 @@ export class ConfigSync extends LiveSyncCommands { LOG_LEVEL_NOTICE, "plugin-reload-" + pluginManifest.id ); - // @ts-ignore - await this.app.plugins.unloadPlugin(pluginManifest.id); - // @ts-ignore - await this.app.plugins.loadPlugin(pluginManifest.id); + await pluginManager.unloadPlugin(pluginManifest.id); + await pluginManager.loadPlugin(pluginManifest.id); this._log( `Plugin reloaded: ${pluginManifest.name}`, LOG_LEVEL_NOTICE, @@ -1200,7 +1193,7 @@ export class ConfigSync extends LiveSyncCommands { const updatedPluginKey = "popupUpdated-plugins"; scheduleTask(updatedPluginKey, 1000, async () => { const popup = await memoIfNotExist(updatedPluginKey, () => new Notice(fragment, 0)); - //@ts-ignore + //@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl. const isShown = popup?.noticeEl?.isShown(); if (!isShown) { memoObject(updatedPluginKey, new Notice(fragment, 0)); @@ -1208,7 +1201,7 @@ export class ConfigSync extends LiveSyncCommands { scheduleTask(updatedPluginKey + "-close", 20000, () => { const popup = retrieveMemoObject(updatedPluginKey); if (!popup) return; - //@ts-ignore + //@ts-ignore -- retained for compatibility with Obsidian versions before Notice.messageEl. if (popup?.noticeEl?.isShown()) { popup.hide(); } @@ -1250,12 +1243,14 @@ export class ConfigSync extends LiveSyncCommands { if (path.toLowerCase().endsWith("/manifest.json")) { const v = readString(new Uint8Array(contentBin)); try { - const json = JSON.parse(v); - if ("version" in json) { - version = `${json.version}`; - } - if ("name" in json) { - displayName = `${json.name}`; + const json: unknown = JSON.parse(v); + if (typeof json === "object" && json !== null) { + if ("version" in json) { + version = String(json.version); + } + if ("name" in json) { + displayName = String(json.name); + } } } catch (ex) { this._log( @@ -1456,7 +1451,7 @@ export class ConfigSync extends LiveSyncCommands { } const oldC = await this.localDatabase.getDBEntryFromMeta(old, false, false); if (oldC) { - const d = (await deserialize(getDocDataAsArray(oldC.data), {})) as PluginDataEx; + const d = deserialize(getDocDataAsArray(oldC.data), {}) as PluginDataEx; if (d.files.length == dt.files.length) { const diffs = d.files .map((previous) => ({ @@ -1709,11 +1704,11 @@ export class ConfigSync extends LiveSyncCommands { return Promise.resolve(true); } - private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) { + private async _allConfigureOptionalSyncFeature(mode: OptionalSyncFeatureMode) { await this.configureHiddenFileSync(mode); return true; } - async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) { + async configureHiddenFileSync(mode: OptionalSyncFeatureMode) { if (mode == "DISABLE") { // this.plugin.settings.usePluginSync = false; // await this.plugin.saveSettings(); diff --git a/src/features/HiddenFileSync/CmdHiddenFileSync.ts b/src/features/HiddenFileSync/CmdHiddenFileSync.ts index 80b72414..7beb2a5e 100644 --- a/src/features/HiddenFileSync/CmdHiddenFileSync.ts +++ b/src/features/HiddenFileSync/CmdHiddenFileSync.ts @@ -1,4 +1,4 @@ -import { type PluginManifest, type ListedFiles } from "@/deps.ts"; +import { type ListedFiles } from "@/deps.ts"; import { type LoadedEntry, type FilePathWithPrefix, @@ -55,20 +55,13 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import type { LiveSyncCore } from "@/main.ts"; import { tryGetFilePath } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts"; +import type { OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts"; +import { getObsidianCommunityPluginManager } from "@/common/obsidianCommunityPlugins.ts"; type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; const HIDDEN_FILE_NOTICE_GROUP = "hidden-file-changes"; const HIDDEN_FILE_NOTICE_DURATION_MS = 20_000; -declare global { - interface OPTIONAL_SYNC_FEATURES { - FETCH: "FETCH"; - OVERWRITE: "OVERWRITE"; - MERGE: "MERGE"; - DISABLE: "DISABLE"; - DISABLE_HIDDEN: "DISABLE_HIDDEN"; - } -} function getComparingMTime( doc: (MetaEntry | LoadedEntry | false) | UXFileInfo | UXStat | null | undefined, includeDeleted = false @@ -1233,11 +1226,10 @@ Offline Changed files: ${files.length}`; const noticeGroups = this.services.context.noticeGroups; let hasNoticeItems = false; try { - //@ts-ignore - const manifests = Object.values(this.app.plugins.manifests) as unknown as PluginManifest[]; - //@ts-ignore - const enabledPlugins = this.app.plugins.enabledPlugins as Set; - const enabledPluginManifests = manifests.filter((e) => enabledPlugins.has(e.id)); + const pluginManager = getObsidianCommunityPluginManager(this.app); + const enabledPluginManifests = pluginManager.manifests.filter((manifest) => + pluginManager.enabledPlugins.has(manifest.id) + ); const modifiedManifests = enabledPluginManifests.filter((e) => updatedFolders.indexOf(e?.dir ?? "") >= 0); for (const manifest of modifiedManifests) { // If notified about plug-ins, reloading Obsidian may not be necessary. @@ -1255,10 +1247,8 @@ Offline Changed files: ${files.length}`; LOG_LEVEL_NOTICE, "plugin-reload-" + updatePluginId ); - // @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API. - await this.app.plugins.unloadPlugin(updatePluginId); - // @ts-ignore -- Obsidian does not expose the plug-in lifecycle methods in its public API. - await this.app.plugins.loadPlugin(updatePluginId); + await pluginManager.unloadPlugin(updatePluginId); + await pluginManager.loadPlugin(updatePluginId); this._log( `Plugin reloaded: ${updatePluginName}`, LOG_LEVEL_NOTICE, @@ -1793,12 +1783,12 @@ Offline Changed files: ${files.length}`; } // --> Configuration handling - private async _allConfigureOptionalSyncFeature(mode: keyof OPTIONAL_SYNC_FEATURES) { + private async _allConfigureOptionalSyncFeature(mode: OptionalSyncFeatureMode) { await this.configureHiddenFileSync(mode); return true; } - async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) { + async configureHiddenFileSync(mode: OptionalSyncFeatureMode) { const result = await configureHiddenFileSyncMode(mode, { disable: async () => { // await this.core.$allSuspendExtraSync(); diff --git a/src/features/HiddenFileSync/configureHiddenFileSyncMode.ts b/src/features/HiddenFileSync/configureHiddenFileSyncMode.ts index 18a08e1c..b9d50972 100644 --- a/src/features/HiddenFileSync/configureHiddenFileSyncMode.ts +++ b/src/features/HiddenFileSync/configureHiddenFileSyncMode.ts @@ -1,4 +1,5 @@ -type HiddenFileSyncMode = "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN"; +import type { HiddenFileSyncMode, OptionalSyncFeatureMode } from "@/features/optionalSyncFeatures.ts"; + type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe"; type ConfigureHiddenFileSyncHandlers = { @@ -9,23 +10,25 @@ type ConfigureHiddenFileSyncHandlers = { export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled"; -function getInitialiseDirection(mode: keyof OPTIONAL_SYNC_FEATURES): HiddenFileSyncDirection | false { +function getInitialiseDirection(mode: HiddenFileSyncMode): HiddenFileSyncDirection | false { if (mode == "FETCH") return "pullForce"; if (mode == "OVERWRITE") return "pushForce"; if (mode == "MERGE") return "safe"; return false; } -function isDisableMode(mode: keyof OPTIONAL_SYNC_FEATURES): boolean { +function isDisableMode( + mode: OptionalSyncFeatureMode +): mode is Extract { return mode == "DISABLE" || mode == "DISABLE_HIDDEN"; } -function isHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES): mode is HiddenFileSyncMode { +function isHiddenFileSyncMode(mode: OptionalSyncFeatureMode): mode is HiddenFileSyncMode { return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode); } export async function configureHiddenFileSyncMode( - mode: keyof OPTIONAL_SYNC_FEATURES, + mode: OptionalSyncFeatureMode, handlers: ConfigureHiddenFileSyncHandlers ): Promise { if (!isHiddenFileSyncMode(mode)) { diff --git a/src/features/optionalSyncFeatures.ts b/src/features/optionalSyncFeatures.ts new file mode 100644 index 00000000..e14c6b91 --- /dev/null +++ b/src/features/optionalSyncFeatures.ts @@ -0,0 +1,27 @@ +export interface OptionalSyncFeatures { + DISABLE: "DISABLE"; + CUSTOMIZE: "CUSTOMIZE"; + DISABLE_CUSTOM: "DISABLE_CUSTOM"; + FETCH: "FETCH"; + OVERWRITE: "OVERWRITE"; + MERGE: "MERGE"; + DISABLE_HIDDEN: "DISABLE_HIDDEN"; +} + +export type OptionalSyncFeatureMode = keyof OptionalSyncFeatures; +export type HiddenFileSyncMode = Extract< + OptionalSyncFeatureMode, + "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN" +>; + +declare global { + interface OPTIONAL_SYNC_FEATURES { + DISABLE: "DISABLE"; + CUSTOMIZE: "CUSTOMIZE"; + DISABLE_CUSTOM: "DISABLE_CUSTOM"; + FETCH: "FETCH"; + OVERWRITE: "OVERWRITE"; + MERGE: "MERGE"; + DISABLE_HIDDEN: "DISABLE_HIDDEN"; + } +} diff --git a/src/modules/core/ReplicateResultProcessor.ts b/src/modules/core/ReplicateResultProcessor.ts index 0b1196e2..2897af6b 100644 --- a/src/modules/core/ReplicateResultProcessor.ts +++ b/src/modules/core/ReplicateResultProcessor.ts @@ -23,6 +23,7 @@ import { serialized } from "octagonal-wheels/concurrency/lock"; import type { ReactiveSource } from "octagonal-wheels/dataobject/reactive_v2"; import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore"; import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import type PouchDB from "pouchdb-core"; const KV_KEY_REPLICATION_RESULT_PROCESSOR_SNAPSHOT = "replicationResultProcessorSnapshot"; type ReplicateResultProcessorState = { diff --git a/src/modules/coreObsidian/storageLib/utilObsidian.ts b/src/modules/coreObsidian/storageLib/utilObsidian.ts index ae2e838c..f37d2ebc 100644 --- a/src/modules/coreObsidian/storageLib/utilObsidian.ts +++ b/src/modules/coreObsidian/storageLib/utilObsidian.ts @@ -12,10 +12,22 @@ import type { UXFileInfoStub, UXFolderInfo, UXInternalFileInfoStub, + UXStat, } from "@vrtmrz/livesync-commonlib/compat/common/types"; import type { LiveSyncCore } from "@/main.ts"; import type { FileAccessObsidian } from "@/serviceModules/FileAccessObsidian.ts"; +function isUXStat(value: unknown): value is UXStat { + if (typeof value !== "object" || value === null) return false; + const stat = value as Partial; + return ( + typeof stat.size === "number" && + typeof stat.ctime === "number" && + typeof stat.mtime === "number" && + (stat.type === "file" || stat.type === "folder") + ); +} + export async function TFileToUXFileInfo( core: LiveSyncCore, file: TFile, @@ -55,8 +67,8 @@ export async function InternalFileToUXFileInfo( prefix: string = ICHeader ): Promise { const name = fullPath.split("/").pop() as string; - const stat = await vaultAccess.tryAdapterStat(fullPath); - if (stat == null) throw new Error(`File not found: ${fullPath}`); + const stat: unknown = await vaultAccess.tryAdapterStat(fullPath); + if (!isUXStat(stat)) throw new Error(`File not found: ${fullPath}`); if (stat.type == "folder") throw new Error(`File not found: ${fullPath}`); const file = await vaultAccess.adapterReadAuto(fullPath); diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 5aa8d54f..4f6a7d92 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -25,6 +25,15 @@ function requestTimeout(timeoutInMs: number = 0): Promise { }); } +function normaliseRequestBody(body: unknown): string | ArrayBuffer | undefined { + if (typeof body === "string" || body instanceof ArrayBuffer) return body; + if (ArrayBuffer.isView(body)) { + if (body.buffer instanceof ArrayBuffer) return body.buffer; + return new Uint8Array(body.buffer).slice().buffer; + } + return undefined; +} + /** * This is close to origin implementation of FetchHttpHandler * https://github.com/aws/aws-sdk-js-v3/blob/main/packages/fetch-http-handler/src/fetch-http-handler.ts @@ -64,7 +73,7 @@ export class ObsHttpHandler extends FetchHttpHandler { urlObj.host = this.reverseProxyNoSignUrl; url = urlObj.href; } - const body = method === "GET" || method === "HEAD" ? undefined : request.body; + const body: unknown = method === "GET" || method === "HEAD" ? undefined : request.body; const transformedHeaders: Record = {}; for (const key of Object.keys(request.headers)) { @@ -80,10 +89,7 @@ export class ObsHttpHandler extends FetchHttpHandler { contentType = transformedHeaders["content-type"]; } - let transformedBody = body; - if (ArrayBuffer.isView(body)) { - transformedBody = new Uint8Array(body.buffer).buffer; - } + const transformedBody = normaliseRequestBody(body); const param: RequestUrlParam = { body: transformedBody, diff --git a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts index d4c71d4f..468a4ee5 100644 --- a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -22,6 +22,7 @@ import { loadDocumentHistoryPreference, saveDocumentHistoryPreference, } from "./documentHistoryPreferences.ts"; +import type PouchDB from "pouchdb-core"; function isImage(path: string) { const ext = path.split(".").splice(-1)[0].toLowerCase(); diff --git a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts index 633795b6..46c6a1d1 100644 --- a/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts +++ b/src/modules/features/InteractiveConflictResolving/ConflictResolveModal.ts @@ -8,7 +8,7 @@ import { globalSlipBoard } from "@vrtmrz/livesync-commonlib/compat/bureau/bureau export type MergeDialogResult = typeof CANCELLED | typeof LEAVE_TO_SUBSEQUENT | string; declare global { - interface Slips extends LSSlips { + interface Slips { "conflict-resolved": typeof CANCELLED | MergeDialogResult; } } diff --git a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts index b2de979b..cbbdab3e 100644 --- a/src/modules/features/ModuleObsidianSettingAsMarkdown.ts +++ b/src/modules/features/ModuleObsidianSettingAsMarkdown.ts @@ -2,8 +2,12 @@ import { isObjectDifferent } from "octagonal-wheels/object"; import { EVENT_SETTING_SAVED, eventHub } from "@/common/events"; import { fireAndForget } from "octagonal-wheels/promises"; -import { DEFAULT_SETTINGS, type FilePathWithPrefix, type ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; -import { parseYaml, stringifyYaml } from "@/deps"; +import { + DEFAULT_SETTINGS, + type FilePathWithPrefix, + type ObsidianLiveSyncSettings, +} from "@vrtmrz/livesync-commonlib/compat/common/types"; +import { parseYaml, stringifyYaml, type Editor, type MarkdownView } from "@/deps"; import { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/logger"; import { AbstractModule } from "@/modules/AbstractModule.ts"; import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; @@ -28,7 +32,7 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule { this.addCommand({ id: "livesync-import-config", name: "Parse setting file", - editorCheckCallback: (checking, editor, ctx) => { + editorCheckCallback: (checking: boolean, editor: Editor, ctx: MarkdownView) => { if (checking) { const doc = editor.getValue(); const ret = this.extractSettingFromWholeText(doc); @@ -104,7 +108,11 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule { const { body } = await this.parseSettingFromMarkdown(filename); let newSetting = {} as Partial; try { - newSetting = parseYaml(body); + const parsed: unknown = parseYaml(body); + if (typeof parsed !== "object" || parsed === null) { + throw new TypeError("The YAML settings must contain an object"); + } + newSetting = parsed; } catch (ex) { this._log("Could not parse YAML", LOG_LEVEL_NOTICE); this._log(ex, LOG_LEVEL_VERBOSE); diff --git a/src/modules/features/ModuleObsidianSettingTab.ts b/src/modules/features/ModuleObsidianSettingTab.ts index 04800d93..983372ba 100644 --- a/src/modules/features/ModuleObsidianSettingTab.ts +++ b/src/modules/features/ModuleObsidianSettingTab.ts @@ -3,6 +3,7 @@ import { AbstractObsidianModule } from "@/modules/AbstractObsidianModule.ts"; // import { PouchDB } from "../../lib/src/pouchdb/pouchdb-browser"; import { EVENT_REQUEST_OPEN_SETTING_WIZARD, EVENT_REQUEST_OPEN_SETTINGS, eventHub } from "@/common/events.ts"; import type { LiveSyncCore } from "@/main.ts"; +import { openObsidianSettings } from "@/common/obsidianSettings.ts"; export class ModuleObsidianSettingDialogue extends AbstractObsidianModule { settingTab!: ObsidianLiveSyncSettingTab; @@ -20,11 +21,7 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule { } openSetting() { - // Undocumented API - //@ts-ignore - this.app.setting.open(); - //@ts-ignore - this.app.setting.openTabById("obsidian-livesync"); + openObsidianSettings(this.app, "obsidian-livesync"); } get appId() { diff --git a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts index 764b9756..9da92c18 100644 --- a/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts +++ b/src/modules/features/SettingDialogue/ObsidianLiveSyncSettingTab.ts @@ -63,6 +63,7 @@ import { paneMaintenance } from "./PaneMaintenance.ts"; import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; import { JournalSyncCore } from "@vrtmrz/livesync-commonlib/compat/replication/journal/JournalSyncCore"; import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replication/journal/objectstore/MinioStorageAdapter"; +import { closeObsidianSettings } from "@/common/obsidianSettings.ts"; // For creating a document // const toc = new Set(); @@ -99,6 +100,14 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { // Buffered Settings for comparing. initialSettings?: typeof this.editingSettings; + private copySettingValue(target: object | undefined, source: object, key: AllSettingItemKey): void { + if (!target) { + throw new Error("Initial settings have not been loaded"); + } + const value: unknown = Reflect.get(source, key); + Reflect.set(target, key, value); + } + /** * Apply editing setting to the plug-in. * @param keys setting keys for applying @@ -111,10 +120,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { // this.initialSettings[k] = this.editingSettings[k]; continue; } - //@ts-ignore - this.core.settings[k] = this.editingSettings[k]; - //@ts-ignore - this.initialSettings[k] = this.core.settings[k]; + this.copySettingValue(this.core.settings, this.editingSettings, k); + this.copySettingValue(this.initialSettings, this.core.settings, k); } keys.forEach((e) => this.refreshSetting(e)); } @@ -149,14 +156,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { appliedKeys.push(k); if (k in OnDialogSettingsDefault) { await this.saveLocalSetting(k as keyof OnDialogSettings); - //@ts-ignore - this.initialSettings[k] = this.editingSettings[k]; + this.copySettingValue(this.initialSettings, this.editingSettings, k); continue; } - //@ts-ignore - this.core.settings[k] = this.editingSettings[k]; - //@ts-ignore - this.initialSettings[k] = this.core.settings[k]; + this.copySettingValue(this.core.settings, this.editingSettings, k); + this.copySettingValue(this.initialSettings, this.core.settings, k); hasChanged = true; } @@ -234,15 +238,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { const localSetting = this.reloadAllLocalSettings(); if (key in this.core.settings) { if (key in localSetting) { - //@ts-ignore - this.initialSettings[key] = localSetting[key]; - //@ts-ignore - this.editingSettings[key] = localSetting[key]; + this.copySettingValue(this.initialSettings, localSetting, key); + this.copySettingValue(this.editingSettings, localSetting, key); } else { - //@ts-ignore - this.initialSettings[key] = this.core.settings[key]; - //@ts-ignore - this.editingSettings[key] = this.initialSettings[key]; + this.copySettingValue(this.initialSettings, this.core.settings, key); + this.copySettingValue(this.editingSettings, this.initialSettings ?? {}, key); } } this.editingSettings = { ...this.editingSettings, ...this.computeAllLocalSettings() }; @@ -308,8 +308,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab { } closeSetting() { - //@ts-ignore : - this.plugin.app.setting.close(); + closeObsidianSettings(this.plugin.app); } handleElement(element: HTMLElement, func: OnUpdateFunc) { diff --git a/src/modules/features/SettingDialogue/PaneChangeLog.ts b/src/modules/features/SettingDialogue/PaneChangeLog.ts index 225460e9..18808754 100644 --- a/src/modules/features/SettingDialogue/PaneChangeLog.ts +++ b/src/modules/features/SettingDialogue/PaneChangeLog.ts @@ -1,7 +1,7 @@ import { MarkdownRenderer } from "@/deps.ts"; import { fireAndForget } from "octagonal-wheels/promises"; import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts"; -//@ts-ignore +declare const UPDATE_INFO: string; const updateInformation: string = UPDATE_INFO || ""; export function paneChangeLog(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement): void { diff --git a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts index 93c02475..12a7ef36 100644 --- a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts +++ b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts @@ -1,12 +1,18 @@ import { requestToCouchDBWithCredentials } from "@/common/utils"; import { $msg } from "@vrtmrz/livesync-commonlib/compat/common/i18n"; -import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger"; +import { + LOG_LEVEL_INFO, + LOG_LEVEL_NOTICE, + LOG_LEVEL_VERBOSE, + Logger, +} from "@vrtmrz/livesync-commonlib/compat/common/logger"; import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { fireAndForget, parseHeaderValues } from "@vrtmrz/livesync-commonlib/compat/common/utils"; import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_couchdb"; import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib"; import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import { normaliseCouchDBConfiguration } from "@/common/couchdbConfiguration"; export const checkConfig = async ( checkResultDiv: HTMLDivElement | undefined, @@ -43,7 +49,7 @@ export const checkConfig = async ( undefined, customHeaders ); - const responseConfig = r.json; + const responseConfig = normaliseCouchDBConfiguration(r.json as unknown); const addConfigFixButton = (title: string, key: string, value: string) => { if (!checkResultDiv) return; diff --git a/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts b/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts index c26ddd08..ed6be090 100644 --- a/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts +++ b/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts @@ -7,6 +7,7 @@ import { isCloudantURI } from "@vrtmrz/livesync-commonlib/compat/pouchdb/utils_c import { generateCredentialObject } from "@vrtmrz/livesync-commonlib/compat/replication/httplib"; import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions"; import { isUnauthorizedError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc"; +import { normaliseCouchDBConfiguration } from "@/common/couchdbConfiguration"; export type ResultMessage = { message: string; classes: string[] }; export type ResultErrorMessage = { message: string; result: "error"; classes: string[] }; @@ -115,7 +116,7 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) => undefined, customHeaders ); - const responseConfig = r.json; + const responseConfig = normaliseCouchDBConfiguration(r.json as unknown); addMessage($msg("obsidianLiveSyncSettingTab.msgNotice"), ["ob-btn-config-head"]); addMessage($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]); addMessage($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]); diff --git a/src/modules/services/ObsidianSettingService.ts b/src/modules/services/ObsidianSettingService.ts index a386c170..a934ebd1 100644 --- a/src/modules/services/ObsidianSettingService.ts +++ b/src/modules/services/ObsidianSettingService.ts @@ -10,10 +10,9 @@ import { } from "@vrtmrz/livesync-commonlib/compat/services/base/SettingService"; import type { ObsidianServiceContext } from "@/modules/services/ObsidianServiceContext"; -export function normaliseObsidianSettingsData( - data: ObsidianLiveSyncSettings | null | undefined -): ObsidianLiveSyncSettings | undefined { - return data ?? undefined; +export function normaliseObsidianSettingsData(data: unknown): ObsidianLiveSyncSettings | undefined { + if (typeof data !== "object" || data === null || Array.isArray(data)) return undefined; + return data as ObsidianLiveSyncSettings; } export class ObsidianSettingService extends SettingService {